diff options
Diffstat (limited to 'src/network/access')
68 files changed, 5929 insertions, 137 deletions
diff --git a/src/network/access/access.pri b/src/network/access/access.pri index 38e9c25269..70ace3f55e 100644 --- a/src/network/access/access.pri +++ b/src/network/access/access.pri @@ -37,7 +37,9 @@ HEADERS += \ access/qnetworkdiskcache.h \ access/qhttpthreaddelegate_p.h \ access/qhttpmultipart.h \ - access/qhttpmultipart_p.h + access/qhttpmultipart_p.h \ + access/qnetworkfile_p.h \ + access/qhttp2protocolhandler_p.h SOURCES += \ access/qftp.cpp \ @@ -68,8 +70,11 @@ SOURCES += \ access/qabstractnetworkcache.cpp \ access/qnetworkdiskcache.cpp \ access/qhttpthreaddelegate.cpp \ - access/qhttpmultipart.cpp + access/qhttpmultipart.cpp \ + access/qnetworkfile.cpp \ + access/qhttp2protocolhandler.cpp mac: LIBS_PRIVATE += -framework Security include($$PWD/../../3rdparty/zlib_dependency.pri) +include($$PWD/http2/http2.pri) diff --git a/src/network/access/http2/bitstreams.cpp b/src/network/access/http2/bitstreams.cpp new file mode 100644 index 0000000000..d22c7cd4ec --- /dev/null +++ b/src/network/access/http2/bitstreams.cpp @@ -0,0 +1,336 @@ +/**************************************************************************** +** +** 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 "bitstreams_p.h" +#include "huffman_p.h" + +#include <QtCore/qbytearray.h> + +#include <limits> + +QT_BEGIN_NAMESPACE + +static_assert(std::numeric_limits<uchar>::digits == 8, "octets expected"); + +namespace HPack +{ + +BitOStream::BitOStream(std::vector<uchar> &b) + : buffer(b), + // All data 'packed' before: + bitsSet(8 * quint64(b.size())) +{ +} + +void BitOStream::writeBits(uchar bits, quint8 bitLength) +{ + Q_ASSERT(bitLength <= 8); + + quint8 count = bitsSet % 8; // bits used in buffer.back(), but 0 means 8 + bits <<= 8 - bitLength; // at top of byte, lower bits clear + if (count) { // we have a part-used byte; fill it some more: + buffer.back() |= bits >> count; + count = 8 - count; + } // count bits have been consumed (and 0 now means 0) + if (bitLength > count) + buffer.push_back(bits << count); + + bitsSet += bitLength; +} + +void BitOStream::write(quint32 src) +{ + const quint8 prefixLen = 8 - bitsSet % 8; + const quint32 fullPrefix = (1 << prefixLen) - 1; + + // https://http2.github.io/http2-spec/compression.html#low-level.representation, + // 5.1 + if (src < fullPrefix) { + writeBits(uchar(src), prefixLen); + } else { + writeBits(uchar(fullPrefix), prefixLen); + // We're on the byte boundary now, + // so we can just 'push_back'. + Q_ASSERT(!(bitsSet % 8)); + src -= fullPrefix; + while (src >= 128) { + buffer.push_back(uchar(src % 128 + 128)); + src /= 128; + bitsSet += 8; + } + buffer.push_back(src); + bitsSet += 8; + } +} + +void BitOStream::write(const QByteArray &src, bool compressed) +{ + quint32 byteLen = src.size(); + if (compressed && byteLen) { + const auto bitLen = huffman_encoded_bit_length(src); + Q_ASSERT(bitLen && std::numeric_limits<quint32>::max() >= (bitLen + 7) / 8); + byteLen = (bitLen + 7) / 8; + writeBits(uchar(1), 1); // bit set - compressed + } else { + writeBits(uchar(0), 1); // no compression. + } + + write(byteLen); + + if (compressed) { + huffman_encode_string(src, *this); + } else { + bitsSet += quint64(src.size()) * 8; + buffer.insert(buffer.end(), src.begin(), src.end()); + } +} + +quint64 BitOStream::bitLength() const +{ + return bitsSet; +} + +quint64 BitOStream::byteLength() const +{ + return buffer.size(); +} + +const uchar *BitOStream::begin() const +{ + return &buffer[0]; +} + +const uchar *BitOStream::end() const +{ + return &buffer[0] + buffer.size(); +} + +void BitOStream::clear() +{ + buffer.clear(); + bitsSet = 0; +} + +BitIStream::BitIStream() + : first(), + last(), + offset(), + streamError(Error::NoError) +{ +} + +BitIStream::BitIStream(const uchar *begin, const uchar *end) + : first(begin), + last(end), + offset(), + streamError(Error::NoError) +{ +} + +quint64 BitIStream::bitLength() const +{ + return quint64(last - first) * 8; +} + +bool BitIStream::hasMoreBits() const +{ + return offset < bitLength(); +} + +bool BitIStream::skipBits(quint64 nBits) +{ + if (nBits > bitLength() || bitLength() - nBits < offset) + return false; + + offset += nBits; + return true; +} + +bool BitIStream::rewindOffset(quint64 nBits) +{ + if (nBits > offset) + return false; + + offset -= nBits; + return true; +} + +bool BitIStream::read(quint32 *dstPtr) +{ + Q_ASSERT(dstPtr); + quint32 &dst = *dstPtr; + + // 5.1 Integer Representation + // + // Integers are used to represent name indexes, header field indexes, or string lengths. + // An integer representation can start anywhere within an octet. + // To allow for optimized processing, an integer representation always finishes at the end of an octet. + // An integer is represented in two parts: a prefix that fills the current octet and an optional + // list of octets that are used if the integer value does not fit within the prefix. + // The number of bits of the prefix (called N) is a parameter of the integer representation. + // If the integer value is small enough, i.e., strictly less than 2N-1, it is compressed within the N-bit prefix. + // ... + // The prefix size, N, is always between 1 and 8 bits. An integer + // starting at an octet boundary will have an 8-bit prefix. + + // Technically, such integers can be of any size, but as we do not have arbitrary-long integers, + // everything that does not fit into 'dst' we consider as an error (after all, try to allocate a string + // of such size and ... hehehe - send it as a part of a header! + + // This function updates the offset _only_ if the read was successful. + if (offset >= bitLength()) { + setError(Error::NotEnoughData); + return false; + } + + setError(Error::NoError); + + const quint32 prefixLen = 8 - offset % 8; + const quint32 fullPrefix = (1 << prefixLen) - 1; + + const uchar prefix = uchar(first[offset / 8] & fullPrefix); + if (prefix < fullPrefix) { + // The number fitted into the prefix bits. + dst = prefix; + offset += prefixLen; + return true; + } + + quint32 newOffset = offset + prefixLen; + // We have a list of bytes representing an integer ... + quint64 val = prefix; + quint32 octetPower = 0; + + while (true) { + if (newOffset >= bitLength()) { + setError(Error::NotEnoughData); + return false; + } + + const uchar octet = first[newOffset / 8]; + + if (octetPower == 28 && octet > 15) { + qCritical("integer is too big"); + setError(Error::InvalidInteger); + return false; + } + + val += quint32(octet & 0x7f) << octetPower; + newOffset += 8; + + if (!(octet & 0x80)) { + // The most significant bit of each octet is used + // as a continuation flag: its value is set to 1 + // except for the last octet in the list. + break; + } + + octetPower += 7; + } + + dst = val; + offset = newOffset; + Q_ASSERT(!(offset % 8)); + + return true; +} + +bool BitIStream::read(QByteArray *dstPtr) +{ + Q_ASSERT(dstPtr); + QByteArray &dst = *dstPtr; + //5.2 String Literal Representation + // + // Header field names and header field values can be represented as string literals. + // A string literal is compressed as a sequence of octets, either by directly encoding + // the string literal's octets or by using a Huffman code. + + // We update the offset _only_ if the read was successful. + + const quint64 oldOffset = offset; + uchar compressed = 0; + if (peekBits(offset, 1, &compressed) != 1 || !skipBits(1)) { + setError(Error::NotEnoughData); + return false; + } + + setError(Error::NoError); + + quint32 len = 0; + if (read(&len)) { + Q_ASSERT(!(offset % 8)); + if (len <= (bitLength() - offset) / 8) { // We have enough data to read a string ... + if (!compressed) { + // Now good news, integer always ends on a byte boundary. + // We can read 'len' bytes without any bit magic. + const char *src = reinterpret_cast<const char *>(first + offset / 8); + dst = QByteArray(src, len); + offset += quint64(len) * 8; + return true; + } + + BitIStream slice(first + offset / 8, first + offset / 8 + len); + if (huffman_decode_string(slice, &dst)) { + offset += quint64(len) * 8; + return true; + } + + setError(Error::CompressionError); + } else { + setError(Error::NotEnoughData); + } + } // else the exact reason was set by read(quint32). + + offset = oldOffset; + return false; +} + +BitIStream::Error BitIStream::error() const +{ + return streamError; +} + +void BitIStream::setError(Error newState) +{ + streamError = newState; +} + +} // namespace HPack + +QT_END_NAMESPACE diff --git a/src/network/access/http2/bitstreams_p.h b/src/network/access/http2/bitstreams_p.h new file mode 100644 index 0000000000..9eba319dc2 --- /dev/null +++ b/src/network/access/http2/bitstreams_p.h @@ -0,0 +1,185 @@ +/**************************************************************************** +** +** 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 BITSTREAMS_P_H +#define BITSTREAMS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qglobal.h> +#include <QtCore/qdebug.h> + +#include <type_traits> +#include <algorithm> +#include <vector> + +QT_BEGIN_NAMESPACE + +class QByteArray; + +namespace HPack +{ + +// BitOStream works with an external buffer, +// for example, HEADERS frame. +class Q_AUTOTEST_EXPORT BitOStream +{ +public: + BitOStream(std::vector<uchar> &buffer); + + // Write 'bitLength' bits from the least significant + // bits in 'bits' to bitstream: + void writeBits(uchar bits, quint8 bitLength); + // HPACK data format, we support: + // * 32-bit integers + // * strings + void write(quint32 src); + void write(const QByteArray &src, bool compressed); + + quint64 bitLength() const; + quint64 byteLength() const; + const uchar *begin() const; + const uchar *end() const; + + void clear(); + +private: + Q_DISABLE_COPY(BitOStream); + + std::vector<uchar> &buffer; + quint64 bitsSet; +}; + +class Q_AUTOTEST_EXPORT BitIStream +{ +public: + // Error is set by 'read' functions. + // 'peek' does not set the error, + // since it just peeks some bits + // without the notion of wrong/right. + // 'read' functions only change 'streamOffset' + // on success. + enum class Error + { + NoError, + NotEnoughData, + CompressionError, + InvalidInteger + }; + + BitIStream(); + BitIStream(const uchar *f, const uchar *l); + + quint64 bitLength() const; + bool hasMoreBits() const; + + // peekBits tries to read 'length' bits from the bitstream into + // 'dst' ('length' must be <= sizeof(dst) * 8), packing them + // starting from the most significant bit of the most significant + // byte. It's a template so that we can use it with different + // integer types. Returns the number of bits actually read. + // Does not change stream's offset. + + template<class T> + quint64 peekBits(quint64 from, quint64 length, T *dstPtr) const + { + static_assert(std::is_unsigned<T>::value, "peekBits: unsigned integer type expected"); + + Q_ASSERT(dstPtr); + Q_ASSERT(length <= sizeof(T) * 8); + + if (from >= bitLength() || !length) + return 0; + + T &dst = *dstPtr; + dst = T(); + length = std::min(length, bitLength() - from); + + const uchar *srcByte = first + from / 8; + auto bitsToRead = length + from % 8; + + while (bitsToRead > 8) { + dst = (dst << 8) | *srcByte; + bitsToRead -= 8; + ++srcByte; + } + + dst <<= bitsToRead; + dst |= *srcByte >> (8 - bitsToRead); + dst <<= sizeof(T) * 8 - length; + + return length; + } + + quint64 streamOffset() const + { + return offset; + } + + bool skipBits(quint64 nBits); + bool rewindOffset(quint64 nBits); + + bool read(quint32 *dstPtr); + bool read(QByteArray *dstPtr); + + Error error() const; + +private: + void setError(Error newState); + + const uchar *first; + const uchar *last; + quint64 offset; + Error streamError; +}; + +} // namespace HPack + +QT_END_NAMESPACE + +#endif diff --git a/src/network/access/http2/hpack.cpp b/src/network/access/http2/hpack.cpp new file mode 100644 index 0000000000..95e6f9051b --- /dev/null +++ b/src/network/access/http2/hpack.cpp @@ -0,0 +1,551 @@ +/**************************************************************************** +** +** 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 "bitstreams_p.h" +#include "hpack_p.h" + +#include <QtCore/qbytearray.h> +#include <QtCore/qdebug.h> + +#include <limits> + +QT_BEGIN_NAMESPACE + +namespace HPack +{ + +HeaderSize header_size(const HttpHeader &header) +{ + HeaderSize size(true, 0); + for (const HeaderField &field : header) { + HeaderSize delta = entry_size(field); + if (!delta.first) + return HeaderSize(); + if (std::numeric_limits<quint32>::max() - size.second < delta.second) + return HeaderSize(); + size.second += delta.second; + } + + return size; +} + +struct BitPattern +{ + BitPattern() + : value(), + bitLength() + { + } + + BitPattern(uchar v, uchar len) + : value(v), + bitLength(len) + { + } + + uchar value; + uchar bitLength; +}; + +bool operator == (const BitPattern &lhs, const BitPattern &rhs) +{ + return lhs.bitLength == rhs.bitLength && lhs.value == rhs.value; +} + +namespace +{ + +using StreamError = BitIStream::Error; + +// There are several bit patterns to distinguish header fields: +// 1 - indexed +// 01 - literal with incremented indexing +// 0000 - literal without indexing +// 0001 - literal, never indexing +// 001 - dynamic table size update. + +// It's always 1 or 0 actually, but the number of bits to extract +// from the input stream - differs. +const BitPattern Indexed(1, 1); +const BitPattern LiteralIncrementalIndexing(1, 2); +const BitPattern LiteralNoIndexing(0, 4); +const BitPattern LiteralNeverIndexing(1, 4); +const BitPattern SizeUpdate(1, 3); + +bool is_literal_field(const BitPattern &pattern) +{ + return pattern == LiteralIncrementalIndexing + || pattern == LiteralNoIndexing + || pattern == LiteralNeverIndexing; +} + +void write_bit_pattern(const BitPattern &pattern, BitOStream &outputStream) +{ + outputStream.writeBits(pattern.value, pattern.bitLength); +} + +bool read_bit_pattern(const BitPattern &pattern, BitIStream &inputStream) +{ + uchar chunk = 0; + + const quint32 bitsRead = inputStream.peekBits(inputStream.streamOffset(), + pattern.bitLength, &chunk); + if (bitsRead != pattern.bitLength) + return false; + + // Since peekBits packs in the most significant bits, shift it! + chunk >>= (8 - bitsRead); + if (chunk != pattern.value) + return false; + + inputStream.skipBits(pattern.bitLength); + + return true; +} + +bool is_request_pseudo_header(const QByteArray &name) +{ + return name == ":method" || name == ":scheme" || + name == ":authority" || name == ":path"; +} + +} // unnamed namespace + +Encoder::Encoder(quint32 size, bool compress) + : lookupTable(size, true /*encoder needs search index*/), + compressStrings(compress) +{ +} + +quint32 Encoder::dynamicTableSize() const +{ + return lookupTable.dynamicDataSize(); +} + +bool Encoder::encodeRequest(BitOStream &outputStream, const HttpHeader &header) +{ + if (!header.size()) { + qDebug("empty header"); + return false; + } + + if (!encodeRequestPseudoHeaders(outputStream, header)) + return false; + + for (const auto &field : header) { + if (is_request_pseudo_header(field.name)) + continue; + + if (!encodeHeaderField(outputStream, field)) + return false; + } + + return true; +} + +bool Encoder::encodeResponse(BitOStream &outputStream, const HttpHeader &header) +{ + if (!header.size()) { + qDebug("empty header"); + return false; + } + + if (!encodeResponsePseudoHeaders(outputStream, header)) + return false; + + for (const auto &field : header) { + if (field.name == ":status") + continue; + + if (!encodeHeaderField(outputStream, field)) + return false; + } + + return true; +} + +bool Encoder::encodeSizeUpdate(BitOStream &outputStream, quint32 newSize) +{ + if (!lookupTable.updateDynamicTableSize(newSize)) { + qDebug("failed to update own table size"); + return false; + } + + write_bit_pattern(SizeUpdate, outputStream); + outputStream.write(newSize); + + return true; +} + +void Encoder::setMaxDynamicTableSize(quint32 size) +{ + // Up to a caller (HTTP2 protocol handler) + // to validate this size first. + lookupTable.setMaxDynamicTableSize(size); +} + +bool Encoder::encodeRequestPseudoHeaders(BitOStream &outputStream, + const HttpHeader &header) +{ + // The following pseudo-header fields are defined for HTTP/2 requests: + // - The :method pseudo-header field includes the HTTP method + // - The :scheme pseudo-header field includes the scheme portion of the target URI + // - The :authority pseudo-header field includes the authority portion of the target URI + // - The :path pseudo-header field includes the path and query parts of the target URI + + // All HTTP/2 requests MUST include exactly one valid value for the :method, + // :scheme, and :path pseudo-header fields, unless it is a CONNECT request + // (Section 8.3). An HTTP request that omits mandatory pseudo-header fields + // is malformed (Section 8.1.2.6). + + using size_type = decltype(header.size()); + + bool methodFound = false; + const char *headerName[] = {":authority", ":scheme", ":path"}; + const size_type nHeaders = sizeof headerName / sizeof headerName[0]; + bool headerFound[nHeaders] = {}; + + for (const auto &field : header) { + if (field.name == ":status") { + qCritical("invalid pseudo-header (:status) in a request"); + return false; + } + + if (field.name == ":method") { + if (methodFound) { + qCritical("only one :method pseudo-header is allowed"); + return false; + } + + if (!encodeMethod(outputStream, field)) + return false; + methodFound = true; + } else if (field.name == "cookie") { + // "crumbs" ... + } else { + for (size_type j = 0; j < nHeaders; ++j) { + if (field.name == headerName[j]) { + if (headerFound[j]) { + qCritical() << "only one" << headerName[j] << "pseudo-header is allowed"; + return false; + } + if (!encodeHeaderField(outputStream, field)) + return false; + headerFound[j] = true; + break; + } + } + } + } + + if (!methodFound) { + qCritical("mandatory :method pseudo-header not found"); + return false; + } + + // 1: don't demand headerFound[0], as :authority isn't mandatory. + for (size_type i = 1; i < nHeaders; ++i) { + if (!headerFound[i]) { + qCritical() << "mandatory" << headerName[i] + << "pseudo-header not found"; + return false; + } + } + + return true; +} + +bool Encoder::encodeHeaderField(BitOStream &outputStream, const HeaderField &field) +{ + // TODO: at the moment we never use LiteralNo/Never Indexing ... + + // Here we try: + // 1. indexed + // 2. literal indexed with indexed name/literal value + // 3. literal indexed with literal name/literal value + if (const auto index = lookupTable.indexOf(field.name, field.value)) + return encodeIndexedField(outputStream, index); + + if (const auto index = lookupTable.indexOf(field.name)) { + return encodeLiteralField(outputStream, LiteralIncrementalIndexing, + index, field.value, compressStrings); + } + + return encodeLiteralField(outputStream, LiteralIncrementalIndexing, + field.name, field.value, compressStrings); +} + +bool Encoder::encodeMethod(BitOStream &outputStream, const HeaderField &field) +{ + Q_ASSERT(field.name == ":method"); + quint32 index = lookupTable.indexOf(field.name, field.value); + if (index) + return encodeIndexedField(outputStream, index); + + index = lookupTable.indexOf(field.name); + Q_ASSERT(index); // ":method" is always in the static table ... + return encodeLiteralField(outputStream, LiteralIncrementalIndexing, + index, field.value, compressStrings); +} + +bool Encoder::encodeResponsePseudoHeaders(BitOStream &outputStream, const HttpHeader &header) +{ + bool statusFound = false; + for (const auto &field : header) { + if (is_request_pseudo_header(field.name)) { + qCritical() << "invalid pseudo-header" << field.name << "in http response"; + return false; + } + + if (field.name == ":status") { + if (statusFound) { + qDebug("only one :status pseudo-header is allowed"); + return false; + } + if (!encodeHeaderField(outputStream, field)) + return false; + statusFound = true; + } else if (field.name == "cookie") { + // "crumbs".. + } + } + + if (!statusFound) + qCritical("mandatory :status pseudo-header not found"); + + return statusFound; +} + +bool Encoder::encodeIndexedField(BitOStream &outputStream, quint32 index) const +{ + Q_ASSERT(lookupTable.indexIsValid(index)); + + write_bit_pattern(Indexed, outputStream); + outputStream.write(index); + + return true; +} + +bool Encoder::encodeLiteralField(BitOStream &outputStream, const BitPattern &fieldType, + const QByteArray &name, const QByteArray &value, + bool withCompression) +{ + Q_ASSERT(is_literal_field(fieldType)); + // According to HPACK, the bit pattern is + // 01 | 000000 (integer 0 that fits into 6-bit prefix), + // since integers always end on byte boundary, + // this also implies that we always start at bit offset == 0. + if (outputStream.bitLength() % 8) { + qCritical("invalid bit offset"); + return false; + } + + if (fieldType == LiteralIncrementalIndexing) { + if (!lookupTable.prependField(name, value)) + qDebug("failed to prepend a new field"); + } + + write_bit_pattern(fieldType, outputStream); + + outputStream.write(0); + outputStream.write(name, withCompression); + outputStream.write(value, withCompression); + + return true; +} + +bool Encoder::encodeLiteralField(BitOStream &outputStream, const BitPattern &fieldType, + quint32 nameIndex, const QByteArray &value, + bool withCompression) +{ + Q_ASSERT(is_literal_field(fieldType)); + + QByteArray name; + const bool found = lookupTable.fieldName(nameIndex, &name); + Q_UNUSED(found) Q_ASSERT(found); + + if (fieldType == LiteralIncrementalIndexing) { + if (!lookupTable.prependField(name, value)) + qDebug("failed to prepend a new field"); + } + + write_bit_pattern(fieldType, outputStream); + outputStream.write(nameIndex); + outputStream.write(value, withCompression); + + return true; +} + +Decoder::Decoder(quint32 size) + : lookupTable{size, false /* we do not need search index ... */} +{ +} + +bool Decoder::decodeHeaderFields(BitIStream &inputStream) +{ + header.clear(); + while (true) { + if (read_bit_pattern(Indexed, inputStream)) { + if (!decodeIndexedField(inputStream)) + return false; + } else if (read_bit_pattern(LiteralIncrementalIndexing, inputStream)) { + if (!decodeLiteralField(LiteralIncrementalIndexing, inputStream)) + return false; + } else if (read_bit_pattern(LiteralNoIndexing, inputStream)) { + if (!decodeLiteralField(LiteralNoIndexing, inputStream)) + return false; + } else if (read_bit_pattern(LiteralNeverIndexing, inputStream)) { + if (!decodeLiteralField(LiteralNeverIndexing, inputStream)) + return false; + } else if (read_bit_pattern(SizeUpdate, inputStream)) { + if (!decodeSizeUpdate(inputStream)) + return false; + } else { + return inputStream.bitLength() == inputStream.streamOffset(); + } + } + + return false; +} + +quint32 Decoder::dynamicTableSize() const +{ + return lookupTable.dynamicDataSize(); +} + +void Decoder::setMaxDynamicTableSize(quint32 size) +{ + // Up to a caller (HTTP2 protocol handler) + // to validate this size first. + lookupTable.setMaxDynamicTableSize(size); +} + +bool Decoder::decodeIndexedField(BitIStream &inputStream) +{ + quint32 index = 0; + if (inputStream.read(&index)) { + if (!index) { + // "The index value of 0 is not used. + // It MUST be treated as a decoding + // error if found in an indexed header + // field representation." + return false; + } + + QByteArray name, value; + if (lookupTable.field(index, &name, &value)) + return processDecodedField(Indexed, name, value); + } else { + handleStreamError(inputStream); + } + + return false; +} + +bool Decoder::decodeSizeUpdate(BitIStream &inputStream) +{ + // For now, just read and skip bits. + quint32 maxSize = 0; + if (inputStream.read(&maxSize)) { + if (!lookupTable.updateDynamicTableSize(maxSize)) + return false; + + return true; + } + + handleStreamError(inputStream); + return false; +} + +bool Decoder::decodeLiteralField(const BitPattern &fieldType, BitIStream &inputStream) +{ + // https://http2.github.io/http2-spec/compression.html + // 6.2.1, 6.2.2, 6.2.3 + // Format for all 'literal' is similar, + // the difference - is how we update/not our lookup table. + quint32 index = 0; + if (inputStream.read(&index)) { + QByteArray name; + if (!index) { + // Read a string. + if (!inputStream.read(&name)) { + handleStreamError(inputStream); + return false; + } + } else { + if (!lookupTable.fieldName(index, &name)) + return false; + } + + QByteArray value; + if (inputStream.read(&value)) + return processDecodedField(fieldType, name, value); + } + + handleStreamError(inputStream); + + return false; +} + +bool Decoder::processDecodedField(const BitPattern &fieldType, + const QByteArray &name, + const QByteArray &value) +{ + if (fieldType == LiteralIncrementalIndexing) { + if (!lookupTable.prependField(name, value)) + return false; + } + + header.push_back(HeaderField(name, value)); + return true; +} + +void Decoder::handleStreamError(BitIStream &inputStream) +{ + const auto errorCode(inputStream.error()); + if (errorCode == StreamError::NoError) + return; + + // For now error handling not needed here, + // HTTP2 layer will end with session error/COMPRESSION_ERROR. +} + +} + +QT_END_NAMESPACE diff --git a/src/network/access/http2/hpack_p.h b/src/network/access/http2/hpack_p.h new file mode 100644 index 0000000000..6a1d30d87b --- /dev/null +++ b/src/network/access/http2/hpack_p.h @@ -0,0 +1,155 @@ +/**************************************************************************** +** +** 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 HPACK_P_H +#define HPACK_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "hpacktable_p.h" + +#include <QtCore/qglobal.h> + +#include <vector> + +QT_BEGIN_NAMESPACE + +class QByteArray; + +namespace HPack +{ + +using HttpHeader = std::vector<HeaderField>; +HeaderSize header_size(const HttpHeader &header); + +class Q_AUTOTEST_EXPORT Encoder +{ +public: + Encoder(quint32 maxTableSize, bool compressStrings); + + quint32 dynamicTableSize() const; + + bool encodeRequest(class BitOStream &outputStream, + const HttpHeader &header); + bool encodeResponse(BitOStream &outputStream, + const HttpHeader &header); + + bool encodeSizeUpdate(BitOStream &outputStream, + quint32 newSize); + + void setMaxDynamicTableSize(quint32 size); + +private: + bool encodeRequestPseudoHeaders(BitOStream &outputStream, + const HttpHeader &header); + bool encodeHeaderField(BitOStream &outputStream, + const HeaderField &field); + bool encodeMethod(BitOStream &outputStream, + const HeaderField &field); + + bool encodeResponsePseudoHeaders(BitOStream &outputStream, + const HttpHeader &header); + + bool encodeIndexedField(BitOStream &outputStream, quint32 index) const; + + + bool encodeLiteralField(BitOStream &outputStream, + const struct BitPattern &fieldType, + quint32 nameIndex, + const QByteArray &value, + bool withCompression); + + bool encodeLiteralField(BitOStream &outputStream, + const BitPattern &fieldType, + const QByteArray &name, + const QByteArray &value, + bool withCompression); + + FieldLookupTable lookupTable; + bool compressStrings; +}; + +class Q_AUTOTEST_EXPORT Decoder +{ +public: + Decoder(quint32 maxTableSize); + + bool decodeHeaderFields(class BitIStream &inputStream); + + const HttpHeader &decodedHeader() const + { + return header; + } + + quint32 dynamicTableSize() const; + + void setMaxDynamicTableSize(quint32 size); + +private: + + bool decodeIndexedField(BitIStream &inputStream); + bool decodeSizeUpdate(BitIStream &inputStream); + bool decodeLiteralField(const BitPattern &fieldType, + BitIStream &inputStream); + + bool processDecodedField(const BitPattern &fieldType, + const QByteArray &name, + const QByteArray &value); + + void handleStreamError(BitIStream &inputStream); + + HttpHeader header; + FieldLookupTable lookupTable; +}; + +} + +QT_END_NAMESPACE + +#endif + diff --git a/src/network/access/http2/hpacktable.cpp b/src/network/access/http2/hpacktable.cpp new file mode 100644 index 0000000000..db9574e2bc --- /dev/null +++ b/src/network/access/http2/hpacktable.cpp @@ -0,0 +1,533 @@ +/**************************************************************************** +** +** 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 "hpacktable_p.h" + +#include <QtCore/qdebug.h> + +#include <algorithm> +#include <cstring> +#include <limits> + + +QT_BEGIN_NAMESPACE + +namespace HPack +{ + +HeaderSize entry_size(const QByteArray &name, const QByteArray &value) +{ + // 32 comes from HPACK: + // "4.1 Calculating Table Size + // Note: The additional 32 octets account for an estimated overhead associated + // with an entry. For example, an entry structure using two 64-bit pointers + // to reference the name and the value of the entry and two 64-bit integers + // for counting the number of references to the name and value would have + // 32 octets of overhead." + + const unsigned sum = unsigned(name.size()) + value.size(); + if (std::numeric_limits<unsigned>::max() - 32 < sum) + return HeaderSize(); + if (sum + 32 > std::numeric_limits<quint32>::max()) + return HeaderSize(); + return HeaderSize(true, quint32(sum + 32)); +} + +namespace +{ + +int compare(const QByteArray &lhs, const QByteArray &rhs) +{ + if (const int minLen = std::min(lhs.size(), rhs.size())) { + // We use memcmp, since strings in headers are allowed + // to contain '\0'. + const int cmp = std::memcmp(lhs.constData(), rhs.constData(), minLen); + if (cmp) + return cmp; + } + + return lhs.size() - rhs.size(); +} + +} // unnamed namespace + +FieldLookupTable::SearchEntry::SearchEntry() + : field(), + chunk(), + offset(), + table() +{ +} + +FieldLookupTable::SearchEntry::SearchEntry(const HeaderField *f, + const Chunk *c, + quint32 o, + const FieldLookupTable *t) + : field(f), + chunk(c), + offset(o), + table(t) +{ + Q_ASSERT(field); +} + +bool FieldLookupTable::SearchEntry::operator < (const SearchEntry &rhs)const +{ + Q_ASSERT(field); + Q_ASSERT(rhs.field); + + int cmp = compare(field->name, rhs.field->name); + if (cmp) + return cmp < 0; + + cmp = compare(field->value, rhs.field->value); + if (cmp) + return cmp < 0; + + if (!chunk) // 'this' is not in the searchIndex. + return rhs.chunk; + + if (!rhs.chunk) // not in the searchIndex. + return false; + + Q_ASSERT(table); + Q_ASSERT(rhs.table == table); + + const quint32 leftChunkIndex = table->indexOfChunk(chunk); + const quint32 rightChunkIndex = rhs.table->indexOfChunk(rhs.chunk); + + // Later added - smaller is chunk index (since we push_front). + if (leftChunkIndex != rightChunkIndex) + return leftChunkIndex > rightChunkIndex; + + // Later added - smaller is offset. + return offset > rhs.offset; +} + +// This data is from HPACK's specs and it's quite +// conveniently sorted == works with binary search as it is. +// Later this can probably change and instead of simple +// vector we'll just reuse FieldLookupTable. +// TODO: it makes sense to generate this table while ... +// configuring/building Qt (some script downloading/parsing/generating +// would be quite handy). +const std::vector<HeaderField> &staticTable() +{ + static std::vector<HeaderField> table = { + {":authority", ""}, + {":method", "GET"}, + {":method", "POST"}, + {":path", "/"}, + {":path", "/index.html"}, + {":scheme", "http"}, + {":scheme", "https"}, + {":status", "200"}, + {":status", "204"}, + {":status", "206"}, + {":status", "304"}, + {":status", "400"}, + {":status", "404"}, + {":status", "500"}, + {"accept-charset", ""}, + {"accept-encoding", "gzip, deflate"}, + {"accept-language", ""}, + {"accept-ranges", ""}, + {"accept", ""}, + {"access-control-allow-origin", ""}, + {"age", ""}, + {"allow", ""}, + {"authorization", ""}, + {"cache-control", ""}, + {"content-disposition", ""}, + {"content-encoding", ""}, + {"content-language", ""}, + {"content-length", ""}, + {"content-location", ""}, + {"content-range", ""}, + {"content-type", ""}, + {"cookie", ""}, + {"date", ""}, + {"etag", ""}, + {"expect", ""}, + {"expires", ""}, + {"from", ""}, + {"host", ""}, + {"if-match", ""}, + {"if-modified-since", ""}, + {"if-none-match", ""}, + {"if-range", ""}, + {"if-unmodified-since", ""}, + {"last-modified", ""}, + {"link", ""}, + {"location", ""}, + {"max-forwards", ""}, + {"proxy-authenticate", ""}, + {"proxy-authorization", ""}, + {"range", ""}, + {"referer", ""}, + {"refresh", ""}, + {"retry-after", ""}, + {"server", ""}, + {"set-cookie", ""}, + {"strict-transport-security", ""}, + {"transfer-encoding", ""}, + {"user-agent", ""}, + {"vary", ""}, + {"via", ""}, + {"www-authenticate", ""} + }; + + return table; +} + +FieldLookupTable::FieldLookupTable(quint32 maxSize, bool use) + : maxTableSize(maxSize), + tableCapacity(maxSize), + useIndex(use), + nDynamic(), + begin(), + end(), + dataSize() +{ +} + + +bool FieldLookupTable::prependField(const QByteArray &name, const QByteArray &value) +{ + const auto entrySize = entry_size(name, value); + if (!entrySize.first) + return false; + + if (entrySize.second > tableCapacity) { + clearDynamicTable(); + return true; + } + + while (nDynamic && tableCapacity - dataSize < entrySize.second) + evictEntry(); + + if (!begin) { + // Either no more space or empty table ... + chunks.push_front(ChunkPtr(new Chunk(ChunkSize))); + end += ChunkSize; + begin = ChunkSize; + } + + --begin; + + dataSize += entrySize.second; + ++nDynamic; + + auto &newField = front(); + newField.name = name; + newField.value = value; + + if (useIndex) { + const auto result = searchIndex.insert(frontKey()); + Q_UNUSED(result) Q_ASSERT(result.second); + } + + return true; +} + +void FieldLookupTable::evictEntry() +{ + if (!nDynamic) + return; + + Q_ASSERT(end != begin); + + if (useIndex) { + const auto res = searchIndex.erase(backKey()); + Q_UNUSED(res) Q_ASSERT(res == 1); + } + + const HeaderField &field = back(); + const auto entrySize = entry_size(field); + Q_ASSERT(entrySize.first); + Q_ASSERT(dataSize >= entrySize.second); + dataSize -= entrySize.second; + + --nDynamic; + --end; + + if (end == begin) { + Q_ASSERT(chunks.size() == 1); + end = ChunkSize; + begin = end; + } else if (!(end % ChunkSize)) { + chunks.pop_back(); + } +} + +quint32 FieldLookupTable::numberOfEntries() const +{ + return quint32(staticTable().size()) + nDynamic; +} + +quint32 FieldLookupTable::numberOfStaticEntries() const +{ + return quint32(staticTable().size()); +} + +quint32 FieldLookupTable::numberOfDynamicEntries() const +{ + return nDynamic; +} + +quint32 FieldLookupTable::dynamicDataSize() const +{ + return dataSize; +} + +void FieldLookupTable::clearDynamicTable() +{ + searchIndex.clear(); + chunks.clear(); + begin = 0; + end = 0; + nDynamic = 0; + dataSize = 0; +} + +bool FieldLookupTable::indexIsValid(quint32 index) const +{ + return index && index <= staticTable().size() + nDynamic; +} + +quint32 FieldLookupTable::indexOf(const QByteArray &name, const QByteArray &value)const +{ + // Start from the static part first: + const auto &table = staticTable(); + const HeaderField field(name, value); + const auto staticPos = std::lower_bound(table.begin(), table.end(), field, + [](const HeaderField &lhs, const HeaderField &rhs) { + int cmp = compare(lhs.name, rhs.name); + if (cmp) + return cmp < 0; + return compare(lhs.value, rhs.value) < 0; + }); + if (staticPos != table.end()) { + if (staticPos->name == name && staticPos->value == value) + return staticPos - table.begin() + 1; + } + + // Now we have to lookup in our dynamic part ... + if (!useIndex) { + qCritical("lookup in dynamic table requires search index enabled"); + return 0; + } + + const SearchEntry key(&field, nullptr, 0, this); + const auto pos = searchIndex.lower_bound(key); + if (pos != searchIndex.end()) { + const HeaderField &found = *pos->field; + if (found.name == name && found.value == value) + return keyToIndex(*pos); + } + + return 0; +} + +quint32 FieldLookupTable::indexOf(const QByteArray &name) const +{ + // Start from the static part first: + const auto &table = staticTable(); + const HeaderField field(name, QByteArray()); + const auto staticPos = std::lower_bound(table.begin(), table.end(), field, + [](const HeaderField &lhs, const HeaderField &rhs) { + return compare(lhs.name, rhs.name) < 0; + }); + if (staticPos != table.end()) { + if (staticPos->name == name) + return staticPos - table.begin() + 1; + } + + // Now we have to lookup in our dynamic part ... + if (!useIndex) { + qCritical("lookup in dynamic table requires search index enabled"); + return 0; + } + + const SearchEntry key(&field, nullptr, 0, this); + const auto pos = searchIndex.lower_bound(key); + if (pos != searchIndex.end()) { + const HeaderField &found = *pos->field; + if (found.name == name) + return keyToIndex(*pos); + } + + return 0; +} + +bool FieldLookupTable::field(quint32 index, QByteArray *name, QByteArray *value) const +{ + Q_ASSERT(name); + Q_ASSERT(value); + + if (!indexIsValid(index)) + return false; + + const auto &table = staticTable(); + if (index - 1 < table.size()) { + *name = table[index - 1].name; + *value = table[index - 1].value; + return true; + } + + index = index - 1 - quint32(table.size()) + begin; + const auto chunkIndex = index / ChunkSize; + Q_ASSERT(chunkIndex < chunks.size()); + const auto offset = index % ChunkSize; + const HeaderField &found = (*chunks[chunkIndex])[offset]; + *name = found.name; + *value = found.value; + + return true; +} + +bool FieldLookupTable::fieldName(quint32 index, QByteArray *dst) const +{ + Q_ASSERT(dst); + return field(index, dst, &dummyDst); +} + +bool FieldLookupTable::fieldValue(quint32 index, QByteArray *dst) const +{ + Q_ASSERT(dst); + return field(index, &dummyDst, dst); +} + +const HeaderField &FieldLookupTable::front() const +{ + Q_ASSERT(nDynamic && begin != end && chunks.size()); + return (*chunks[0])[begin]; +} + +HeaderField &FieldLookupTable::front() +{ + Q_ASSERT(nDynamic && begin != end && chunks.size()); + return (*chunks[0])[begin]; +} + +const HeaderField &FieldLookupTable::back() const +{ + Q_ASSERT(nDynamic && end && end != begin); + + const quint32 absIndex = end - 1; + const quint32 chunkIndex = absIndex / ChunkSize; + Q_ASSERT(chunkIndex < chunks.size()); + const quint32 offset = absIndex % ChunkSize; + return (*chunks[chunkIndex])[offset]; +} + +quint32 FieldLookupTable::indexOfChunk(const Chunk *chunk) const +{ + Q_ASSERT(chunk); + + for (size_type i = 0; i < chunks.size(); ++i) { + if (chunks[i].get() == chunk) + return quint32(i); + } + + Q_UNREACHABLE(); + return 0; +} + +quint32 FieldLookupTable::keyToIndex(const SearchEntry &key) const +{ + Q_ASSERT(key.chunk); + + const auto chunkIndex = indexOfChunk(key.chunk); + const auto offset = key.offset; + Q_ASSERT(offset < ChunkSize); + Q_ASSERT(chunkIndex || offset >= begin); + + return quint32(offset + chunkIndex * ChunkSize - begin + 1 + staticTable().size()); +} + +FieldLookupTable::SearchEntry FieldLookupTable::frontKey() const +{ + Q_ASSERT(chunks.size() && end != begin); + return SearchEntry(&front(), chunks.front().get(), begin, this); +} + +FieldLookupTable::SearchEntry FieldLookupTable::backKey() const +{ + Q_ASSERT(chunks.size() && end != begin); + + const HeaderField &field = back(); + const quint32 absIndex = end - 1; + const auto offset = absIndex % ChunkSize; + const auto chunk = chunks[absIndex / ChunkSize].get(); + + return SearchEntry(&field, chunk, offset, this); +} + +bool FieldLookupTable::updateDynamicTableSize(quint32 size) +{ + if (!size) { + clearDynamicTable(); + return true; + } + + if (size > maxTableSize) + return false; + + tableCapacity = size; + while (nDynamic && dataSize > tableCapacity) + evictEntry(); + + return true; +} + +void FieldLookupTable::setMaxDynamicTableSize(quint32 size) +{ + // This is for an external user, for example, HTTP2 protocol + // layer that can receive SETTINGS frame from its peer. + // No validity checks here, up to this external user. + // We update max size and capacity (this can also result in + // items evicted or even dynamic table completely cleared). + maxTableSize = size; + updateDynamicTableSize(size); +} + +} + +QT_END_NAMESPACE diff --git a/src/network/access/http2/hpacktable_p.h b/src/network/access/http2/hpacktable_p.h new file mode 100644 index 0000000000..aaea89b986 --- /dev/null +++ b/src/network/access/http2/hpacktable_p.h @@ -0,0 +1,237 @@ +/**************************************************************************** +** +** 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 HPACKTABLE_P_H +#define HPACKTABLE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qbytearray.h> +#include <QtCore/qglobal.h> +#include <QtCore/qpair.h> + +#include <vector> +#include <memory> +#include <deque> +#include <set> + +QT_BEGIN_NAMESPACE + +namespace HPack +{ + +struct Q_AUTOTEST_EXPORT HeaderField +{ + HeaderField() + { + } + + HeaderField(const QByteArray &n, const QByteArray &v) + : name(n), + value(v) + { + } + + bool operator == (const HeaderField &rhs) const + { + return name == rhs.name && value == rhs.value; + } + + QByteArray name; + QByteArray value; +}; + +using HeaderSize = QPair<bool, quint32>; + +HeaderSize entry_size(const QByteArray &name, const QByteArray &value); + +inline HeaderSize entry_size(const HeaderField &entry) +{ + return entry_size(entry.name, entry.value); +} + +/* + Lookup table consists of two parts (HPACK, 2.3): + the immutable static table (pre-defined by HPACK's specs) + and dynamic table which is updated while + compressing/decompressing headers. + + Table must provide/implement: + 1. Fast random access - we read fields' indices from + HPACK's bit stream. + 2. FIFO for dynamic part - to push new items to the front + and evict them from the back (HPACK, 2.3.2). + 3. Fast lookup - encoder receives pairs of strings + (name|value) and it has to find an index for a pair + as the whole or for a name at least (if it's already + in either static or dynamic table). + + Static table is an immutable vector. + + Dynamic part is implemented in a way similar to std::deque - + it's a vector of pointers to chunks. Each chunk is a vector of + (name|value) pairs. Once allocated with a fixed size, chunk + never re-allocates its data, so entries' addresses do not change. + We add new chunks prepending them to the front of a vector, + in each chunk we fill (name|value) pairs starting from the back + of the chunk (this simplifies item eviction/FIFO). + Given a 'linear' index we can find a chunk number and + offset in this chunk - random access. + + Lookup in a static part is straightforward: + it's an (immutable) vector, data is sorted, + contains no duplicates, we use binary search comparing string values. + + To provide a lookup in dynamic table faster than a linear search, + we have an std::set of 'SearchEntries', where each entry contains: + - a pointer to a (name|value) pair (to compare + name|value strings). + - a pointer to a chunk containing this pair and + - an offset within this chunk - to calculate a + 'linear' index. + + Entries in a table can be duplicated (HPACK, 2.3.2), + if we evict an entry, we must update our index removing + the exactly right key, thus keys in this set are sorted + by name|value pairs first, and then by chunk index/offset + (so that NewSearchEntryKey < OldSearchEntry even if strings + are equal). +*/ + +class Q_AUTOTEST_EXPORT FieldLookupTable +{ +public: + enum + { + ChunkSize = 16, + DefaultSize = 4096 // Recommended by HTTP2. + }; + + FieldLookupTable(quint32 maxTableSize, bool useIndex); + + bool prependField(const QByteArray &name, const QByteArray &value); + void evictEntry(); + + quint32 numberOfEntries() const; + quint32 numberOfStaticEntries() const; + quint32 numberOfDynamicEntries() const; + quint32 dynamicDataSize() const; + void clearDynamicTable(); + + bool indexIsValid(quint32 index) const; + quint32 indexOf(const QByteArray &name, const QByteArray &value) const; + quint32 indexOf(const QByteArray &name) const; + bool field(quint32 index, QByteArray *name, QByteArray *value) const; + bool fieldName(quint32 index, QByteArray *dst) const; + bool fieldValue(quint32 index, QByteArray *dst) const; + + bool updateDynamicTableSize(quint32 size); + void setMaxDynamicTableSize(quint32 size); + +private: + // Table's maximum size is controlled + // by SETTINGS_HEADER_TABLE_SIZE (HTTP/2, 6.5.2). + quint32 maxTableSize; + // The tableCapacity is how many bytes the table + // can currently hold. It cannot exceed maxTableSize. + // It can be modified by a special message in + // the HPACK bit stream (HPACK, 6.3). + quint32 tableCapacity; + + using Chunk = std::vector<HeaderField>; + using ChunkPtr = std::unique_ptr<Chunk>; + std::deque<ChunkPtr> chunks; + using size_type = std::deque<ChunkPtr>::size_type; + + struct SearchEntry; + friend struct SearchEntry; + + struct SearchEntry + { + SearchEntry(); + SearchEntry(const HeaderField *f, const Chunk *c, + quint32 o, const FieldLookupTable *t); + + const HeaderField *field; + const Chunk *chunk; + const quint32 offset; + const FieldLookupTable *table; + + bool operator < (const SearchEntry &rhs) const; + }; + + bool useIndex; + std::set<SearchEntry> searchIndex; + + SearchEntry frontKey() const; + SearchEntry backKey() const; + + bool fieldAt(quint32 index, HeaderField *field) const; + + const HeaderField &front() const; + HeaderField &front(); + const HeaderField &back() const; + + quint32 nDynamic; + quint32 begin; + quint32 end; + quint32 dataSize; + + quint32 indexOfChunk(const Chunk *chunk) const; + quint32 keyToIndex(const SearchEntry &key) const; + + mutable QByteArray dummyDst; + + Q_DISABLE_COPY(FieldLookupTable); +}; + +} + +QT_END_NAMESPACE + +#endif diff --git a/src/network/access/http2/http2.pri b/src/network/access/http2/http2.pri new file mode 100644 index 0000000000..e9f30aeb4a --- /dev/null +++ b/src/network/access/http2/http2.pri @@ -0,0 +1,17 @@ +HEADERS += \ + access/http2/bitstreams_p.h \ + access/http2/huffman_p.h \ + access/http2/hpack_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/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..55e9f93b19 --- /dev/null +++ b/src/network/access/http2/http2frames.cpp @@ -0,0 +1,529 @@ +/**************************************************************************** +** +** 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 <QtNetwork/qabstractsocket.h> + +#include <algorithm> +#include <utility> + +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: + // 6.7 PING + if (payloadSize != 8) + return FrameStatus::sizeError; + break; + case FrameType::GOAWAY: + // 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<uchar> &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<quint32>(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<char*>(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<quint32>(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<char *>(&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<quint32>(&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<const char *>(&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<const char *>(&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<const char *>(&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<const char*>(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..6abed315ca --- /dev/null +++ b/src/network/access/http2/http2frames_p.h @@ -0,0 +1,184 @@ +/**************************************************************************** +** +** 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 + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the Network Access API. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include "http2protocol_p.h" +#include "hpack_p.h" + +#include <QtCore/qendian.h> +#include <QtCore/qglobal.h> + +#include <algorithm> +#include <vector> + +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<uchar> framePayload; +}; + +class Q_AUTOTEST_EXPORT FrameWriter +{ + friend class QT_PREPEND_NAMESPACE(QHttp2ProtocolHandler); + +public: + using payload_type = std::vector<uchar>; + 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<typename ValueType> + 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<uchar> &rawFrameBuffer() + { + return frameBuffer; + } + +private: + void updatePayloadSize(); + std::vector<uchar> 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 <QtCore/qstring.h> + +#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..5c46949e23 --- /dev/null +++ b/src/network/access/http2/http2protocol_p.h @@ -0,0 +1,175 @@ +/**************************************************************************** +** +** 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 + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the Network Access API. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include <QtNetwork/qnetworkreply.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qglobal.h> + +// 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 <QtCore/qdebug.h> + +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..5d9a6ab512 --- /dev/null +++ b/src/network/access/http2/http2streams_p.h @@ -0,0 +1,103 @@ +/**************************************************************************** +** +** 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 + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the Network Access API. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qhttpnetworkconnectionchannel_p.h> +#include <private/qhttpnetworkrequest_p.h> + +#include <QtCore/qglobal.h> + +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/http2/huffman.cpp b/src/network/access/http2/huffman.cpp new file mode 100644 index 0000000000..0c1aa54dd6 --- /dev/null +++ b/src/network/access/http2/huffman.cpp @@ -0,0 +1,573 @@ +/**************************************************************************** +** +** 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 "bitstreams_p.h" +#include "huffman_p.h" + +#include <QtCore/qbytearray.h> + +#include <algorithm> +#include <limits> + +QT_BEGIN_NAMESPACE + +namespace HPack +{ + +/* + The static Huffman code used here was extracted from: + https://http2.github.io/http2-spec/compression.html#huffman.code + + This code was generated from statistics obtained on a large + sample of HTTP headers. It is a canonical Huffman code + with some tweaking to ensure that no symbol has a unique + code length. All codes were left-aligned - for implementation + convenience. + + Using binary trees to implement decoding would be prohibitively + expensive (both memory and time-wise). Instead we use a table-based + approach and any given code itself works as an index into such table(s). + We have 256 possible byte values and code lengths in + a range [5, 26]. This would require a huge table (and most of entries + would be 'wasted', since we only have to encode 256 elements). + Instead we use multi-level tables. The first level table + is using 9-bit length index; some entries in this table are 'terminal', + some reference the next level table(s). + + For example, bytes with values 48 and 49 (ASCII codes for '0' and '1') + both have code length 5, Huffman codes are: 00000 and 00001. They + both are placed in the 'root' table, + the 'root' table has index length == 9: + [00000 | 4 remaining bits] + ... + [00001 | 4 remaining bits] + + All entires with indices between these two will 'point' to value 48 + with bitLength == 5 so that bit stream (for example) 000001010 will be + decoded as: 48 + "put 1010 back into bitstream". + + A good description can be found here: + http://commandlinefanatic.com/cgi-bin/showarticle.cgi?article=art007 + or just google "Efficient Huffman Decoding". + Also see comments below about 'filling holes'. +*/ + +namespace +{ + +const CodeEntry staticHuffmanCodeTable[] +{ + { 0, 0xffc00000ul, 13}, // 11111111|11000 + { 1, 0xffffb000ul, 23}, // 11111111|11111111|1011000 + { 2, 0xfffffe20ul, 28}, // 11111111|11111111|11111110|0010 + { 3, 0xfffffe30ul, 28}, // 11111111|11111111|11111110|0011 + { 4, 0xfffffe40ul, 28}, // 11111111|11111111|11111110|0100 + { 5, 0xfffffe50ul, 28}, // 11111111|11111111|11111110|0101 + { 6, 0xfffffe60ul, 28}, // 11111111|11111111|11111110|0110 + { 7, 0xfffffe70ul, 28}, // 11111111|11111111|11111110|0111 + { 8, 0xfffffe80ul, 28}, // 11111111|11111111|11111110|1000 + { 9, 0xffffea00ul, 24}, // 11111111|11111111|11101010 + { 10, 0xfffffff0ul, 30}, // 11111111|11111111|11111111|111100 + { 11, 0xfffffe90ul, 28}, // 11111111|11111111|11111110|1001 + { 12, 0xfffffea0ul, 28}, // 11111111|11111111|11111110|1010 + { 13, 0xfffffff4ul, 30}, // 11111111|11111111|11111111|111101 + { 14, 0xfffffeb0ul, 28}, // 11111111|11111111|11111110|1011 + { 15, 0xfffffec0ul, 28}, // 11111111|11111111|11111110|1100 + { 16, 0xfffffed0ul, 28}, // 11111111|11111111|11111110|1101 + { 17, 0xfffffee0ul, 28}, // 11111111|11111111|11111110|1110 + { 18, 0xfffffef0ul, 28}, // 11111111|11111111|11111110|1111 + { 19, 0xffffff00ul, 28}, // 11111111|11111111|11111111|0000 + { 20, 0xffffff10ul, 28}, // 11111111|11111111|11111111|0001 + { 21, 0xffffff20ul, 28}, // 11111111|11111111|11111111|0010 + { 22, 0xfffffff8ul, 30}, // 11111111|11111111|11111111|111110 + { 23, 0xffffff30ul, 28}, // 11111111|11111111|11111111|0011 + { 24, 0xffffff40ul, 28}, // 11111111|11111111|11111111|0100 + { 25, 0xffffff50ul, 28}, // 11111111|11111111|11111111|0101 + { 26, 0xffffff60ul, 28}, // 11111111|11111111|11111111|0110 + { 27, 0xffffff70ul, 28}, // 11111111|11111111|11111111|0111 + { 28, 0xffffff80ul, 28}, // 11111111|11111111|11111111|1000 + { 29, 0xffffff90ul, 28}, // 11111111|11111111|11111111|1001 + { 30, 0xffffffa0ul, 28}, // 11111111|11111111|11111111|1010 + { 31, 0xffffffb0ul, 28}, // 11111111|11111111|11111111|1011 + { 32, 0x50000000ul, 6}, // ' ' 010100 + { 33, 0xfe000000ul, 10}, // '!' 11111110|00 + { 34, 0xfe400000ul, 10}, // '"' 11111110|01 + { 35, 0xffa00000ul, 12}, // '#' 11111111|1010 + { 36, 0xffc80000ul, 13}, // '$' 11111111|11001 + { 37, 0x54000000ul, 6}, // '%' 010101 + { 38, 0xf8000000ul, 8}, // '&' 11111000 + { 39, 0xff400000ul, 11}, // ''' 11111111|010 + { 40, 0xfe800000ul, 10}, // '(' 11111110|10 + { 41, 0xfec00000ul, 10}, // ')' 11111110|11 + { 42, 0xf9000000ul, 8}, // '*' 11111001 + { 43, 0xff600000ul, 11}, // '+' 11111111|011 + { 44, 0xfa000000ul, 8}, // ',' 11111010 + { 45, 0x58000000ul, 6}, // '-' 010110 + { 46, 0x5c000000ul, 6}, // '.' 010111 + { 47, 0x60000000ul, 6}, // '/' 011000 + { 48, 0x00000000ul, 5}, // '0' 00000 + { 49, 0x08000000ul, 5}, // '1' 00001 + { 50, 0x10000000ul, 5}, // '2' 00010 + { 51, 0x64000000ul, 6}, // '3' 011001 + { 52, 0x68000000ul, 6}, // '4' 011010 + { 53, 0x6c000000ul, 6}, // '5' 011011 + { 54, 0x70000000ul, 6}, // '6' 011100 + { 55, 0x74000000ul, 6}, // '7' 011101 + { 56, 0x78000000ul, 6}, // '8' 011110 + { 57, 0x7c000000ul, 6}, // '9' 011111 + { 58, 0xb8000000ul, 7}, // ':' 1011100 + { 59, 0xfb000000ul, 8}, // ';' 11111011 + { 60, 0xfff80000ul, 15}, // '<' 11111111|1111100 + { 61, 0x80000000ul, 6}, // '=' 100000 + { 62, 0xffb00000ul, 12}, // '>' 11111111|1011 + { 63, 0xff000000ul, 10}, // '?' 11111111|00 + { 64, 0xffd00000ul, 13}, // '@' 11111111|11010 + { 65, 0x84000000ul, 6}, // 'A' 100001 + { 66, 0xba000000ul, 7}, // 'B' 1011101 + { 67, 0xbc000000ul, 7}, // 'C' 1011110 + { 68, 0xbe000000ul, 7}, // 'D' 1011111 + { 69, 0xc0000000ul, 7}, // 'E' 1100000 + { 70, 0xc2000000ul, 7}, // 'F' 1100001 + { 71, 0xc4000000ul, 7}, // 'G' 1100010 + { 72, 0xc6000000ul, 7}, // 'H' 1100011 + { 73, 0xc8000000ul, 7}, // 'I' 1100100 + { 74, 0xca000000ul, 7}, // 'J' 1100101 + { 75, 0xcc000000ul, 7}, // 'K' 1100110 + { 76, 0xce000000ul, 7}, // 'L' 1100111 + { 77, 0xd0000000ul, 7}, // 'M' 1101000 + { 78, 0xd2000000ul, 7}, // 'N' 1101001 + { 79, 0xd4000000ul, 7}, // 'O' 1101010 + { 80, 0xd6000000ul, 7}, // 'P' 1101011 + { 81, 0xd8000000ul, 7}, // 'Q' 1101100 + { 82, 0xda000000ul, 7}, // 'R' 1101101 + { 83, 0xdc000000ul, 7}, // 'S' 1101110 + { 84, 0xde000000ul, 7}, // 'T' 1101111 + { 85, 0xe0000000ul, 7}, // 'U' 1110000 + { 86, 0xe2000000ul, 7}, // 'V' 1110001 + { 87, 0xe4000000ul, 7}, // 'W' 1110010 + { 88, 0xfc000000ul, 8}, // 'X' 11111100 + { 89, 0xe6000000ul, 7}, // 'Y' 1110011 + { 90, 0xfd000000ul, 8}, // 'Z' 11111101 + { 91, 0xffd80000ul, 13}, // '[' 11111111|11011 + { 92, 0xfffe0000ul, 19}, // '\' 11111111|11111110|000 + { 93, 0xffe00000ul, 13}, // ']' 11111111|11100 + { 94, 0xfff00000ul, 14}, // '^' 11111111|111100 + { 95, 0x88000000ul, 6}, // '_' 100010 + { 96, 0xfffa0000ul, 15}, // '`' 11111111|1111101 + { 97, 0x18000000ul, 5}, // 'a' 00011 + { 98, 0x8c000000ul, 6}, // 'b' 100011 + { 99, 0x20000000ul, 5}, // 'c' 00100 + {100, 0x90000000ul, 6}, // 'd' 100100 + {101, 0x28000000ul, 5}, // 'e' 00101 + {102, 0x94000000ul, 6}, // 'f' 100101 + {103, 0x98000000ul, 6}, // 'g' 100110 + {104, 0x9c000000ul, 6}, // 'h' 100111 + {105, 0x30000000ul, 5}, // 'i' 00110 + {106, 0xe8000000ul, 7}, // 'j' 1110100 + {107, 0xea000000ul, 7}, // 'k' 1110101 + {108, 0xa0000000ul, 6}, // 'l' 101000 + {109, 0xa4000000ul, 6}, // 'm' 101001 + {110, 0xa8000000ul, 6}, // 'n' 101010 + {111, 0x38000000ul, 5}, // 'o' 00111 + {112, 0xac000000ul, 6}, // 'p' 101011 + {113, 0xec000000ul, 7}, // 'q' 1110110 + {114, 0xb0000000ul, 6}, // 'r' 101100 + {115, 0x40000000ul, 5}, // 's' 01000 + {116, 0x48000000ul, 5}, // 't' 01001 + {117, 0xb4000000ul, 6}, // 'u' 101101 + {118, 0xee000000ul, 7}, // 'v' 1110111 + {119, 0xf0000000ul, 7}, // 'w' 1111000 + {120, 0xf2000000ul, 7}, // 'x' 1111001 + {121, 0xf4000000ul, 7}, // 'y' 1111010 + {122, 0xf6000000ul, 7}, // 'z' 1111011 + {123, 0xfffc0000ul, 15}, // '{' 11111111|1111110 + {124, 0xff800000ul, 11}, // '|' 11111111|100 + {125, 0xfff40000ul, 14}, // '}' 11111111|111101 + {126, 0xffe80000ul, 13}, // '~' 11111111|11101 + {127, 0xffffffc0ul, 28}, // 11111111|11111111|11111111|1100 + {128, 0xfffe6000ul, 20}, // 11111111|11111110|0110 + {129, 0xffff4800ul, 22}, // 11111111|11111111|010010 + {130, 0xfffe7000ul, 20}, // 11111111|11111110|0111 + {131, 0xfffe8000ul, 20}, // 11111111|11111110|1000 + {132, 0xffff4c00ul, 22}, // 11111111|11111111|010011 + {133, 0xffff5000ul, 22}, // 11111111|11111111|010100 + {134, 0xffff5400ul, 22}, // 11111111|11111111|010101 + {135, 0xffffb200ul, 23}, // 11111111|11111111|1011001 + {136, 0xffff5800ul, 22}, // 11111111|11111111|010110 + {137, 0xffffb400ul, 23}, // 11111111|11111111|1011010 + {138, 0xffffb600ul, 23}, // 11111111|11111111|1011011 + {139, 0xffffb800ul, 23}, // 11111111|11111111|1011100 + {140, 0xffffba00ul, 23}, // 11111111|11111111|1011101 + {141, 0xffffbc00ul, 23}, // 11111111|11111111|1011110 + {142, 0xffffeb00ul, 24}, // 11111111|11111111|11101011 + {143, 0xffffbe00ul, 23}, // 11111111|11111111|1011111 + {144, 0xffffec00ul, 24}, // 11111111|11111111|11101100 + {145, 0xffffed00ul, 24}, // 11111111|11111111|11101101 + {146, 0xffff5c00ul, 22}, // 11111111|11111111|010111 + {147, 0xffffc000ul, 23}, // 11111111|11111111|1100000 + {148, 0xffffee00ul, 24}, // 11111111|11111111|11101110 + {149, 0xffffc200ul, 23}, // 11111111|11111111|1100001 + {150, 0xffffc400ul, 23}, // 11111111|11111111|1100010 + {151, 0xffffc600ul, 23}, // 11111111|11111111|1100011 + {152, 0xffffc800ul, 23}, // 11111111|11111111|1100100 + {153, 0xfffee000ul, 21}, // 11111111|11111110|11100 + {154, 0xffff6000ul, 22}, // 11111111|11111111|011000 + {155, 0xffffca00ul, 23}, // 11111111|11111111|1100101 + {156, 0xffff6400ul, 22}, // 11111111|11111111|011001 + {157, 0xffffcc00ul, 23}, // 11111111|11111111|1100110 + {158, 0xffffce00ul, 23}, // 11111111|11111111|1100111 + {159, 0xffffef00ul, 24}, // 11111111|11111111|11101111 + {160, 0xffff6800ul, 22}, // 11111111|11111111|011010 + {161, 0xfffee800ul, 21}, // 11111111|11111110|11101 + {162, 0xfffe9000ul, 20}, // 11111111|11111110|1001 + {163, 0xffff6c00ul, 22}, // 11111111|11111111|011011 + {164, 0xffff7000ul, 22}, // 11111111|11111111|011100 + {165, 0xffffd000ul, 23}, // 11111111|11111111|1101000 + {166, 0xffffd200ul, 23}, // 11111111|11111111|1101001 + {167, 0xfffef000ul, 21}, // 11111111|11111110|11110 + {168, 0xffffd400ul, 23}, // 11111111|11111111|1101010 + {169, 0xffff7400ul, 22}, // 11111111|11111111|011101 + {170, 0xffff7800ul, 22}, // 11111111|11111111|011110 + {171, 0xfffff000ul, 24}, // 11111111|11111111|11110000 + {172, 0xfffef800ul, 21}, // 11111111|11111110|11111 + {173, 0xffff7c00ul, 22}, // 11111111|11111111|011111 + {174, 0xffffd600ul, 23}, // 11111111|11111111|1101011 + {175, 0xffffd800ul, 23}, // 11111111|11111111|1101100 + {176, 0xffff0000ul, 21}, // 11111111|11111111|00000 + {177, 0xffff0800ul, 21}, // 11111111|11111111|00001 + {178, 0xffff8000ul, 22}, // 11111111|11111111|100000 + {179, 0xffff1000ul, 21}, // 11111111|11111111|00010 + {180, 0xffffda00ul, 23}, // 11111111|11111111|1101101 + {181, 0xffff8400ul, 22}, // 11111111|11111111|100001 + {182, 0xffffdc00ul, 23}, // 11111111|11111111|1101110 + {183, 0xffffde00ul, 23}, // 11111111|11111111|1101111 + {184, 0xfffea000ul, 20}, // 11111111|11111110|1010 + {185, 0xffff8800ul, 22}, // 11111111|11111111|100010 + {186, 0xffff8c00ul, 22}, // 11111111|11111111|100011 + {187, 0xffff9000ul, 22}, // 11111111|11111111|100100 + {188, 0xffffe000ul, 23}, // 11111111|11111111|1110000 + {189, 0xffff9400ul, 22}, // 11111111|11111111|100101 + {190, 0xffff9800ul, 22}, // 11111111|11111111|100110 + {191, 0xffffe200ul, 23}, // 11111111|11111111|1110001 + {192, 0xfffff800ul, 26}, // 11111111|11111111|11111000|00 + {193, 0xfffff840ul, 26}, // 11111111|11111111|11111000|01 + {194, 0xfffeb000ul, 20}, // 11111111|11111110|1011 + {195, 0xfffe2000ul, 19}, // 11111111|11111110|001 + {196, 0xffff9c00ul, 22}, // 11111111|11111111|100111 + {197, 0xffffe400ul, 23}, // 11111111|11111111|1110010 + {198, 0xffffa000ul, 22}, // 11111111|11111111|101000 + {199, 0xfffff600ul, 25}, // 11111111|11111111|11110110|0 + {200, 0xfffff880ul, 26}, // 11111111|11111111|11111000|10 + {201, 0xfffff8c0ul, 26}, // 11111111|11111111|11111000|11 + {202, 0xfffff900ul, 26}, // 11111111|11111111|11111001|00 + {203, 0xfffffbc0ul, 27}, // 11111111|11111111|11111011|110 + {204, 0xfffffbe0ul, 27}, // 11111111|11111111|11111011|111 + {205, 0xfffff940ul, 26}, // 11111111|11111111|11111001|01 + {206, 0xfffff100ul, 24}, // 11111111|11111111|11110001 + {207, 0xfffff680ul, 25}, // 11111111|11111111|11110110|1 + {208, 0xfffe4000ul, 19}, // 11111111|11111110|010 + {209, 0xffff1800ul, 21}, // 11111111|11111111|00011 + {210, 0xfffff980ul, 26}, // 11111111|11111111|11111001|10 + {211, 0xfffffc00ul, 27}, // 11111111|11111111|11111100|000 + {212, 0xfffffc20ul, 27}, // 11111111|11111111|11111100|001 + {213, 0xfffff9c0ul, 26}, // 11111111|11111111|11111001|11 + {214, 0xfffffc40ul, 27}, // 11111111|11111111|11111100|010 + {215, 0xfffff200ul, 24}, // 11111111|11111111|11110010 + {216, 0xffff2000ul, 21}, // 11111111|11111111|00100 + {217, 0xffff2800ul, 21}, // 11111111|11111111|00101 + {218, 0xfffffa00ul, 26}, // 11111111|11111111|11111010|00 + {219, 0xfffffa40ul, 26}, // 11111111|11111111|11111010|01 + {220, 0xffffffd0ul, 28}, // 11111111|11111111|11111111|1101 + {221, 0xfffffc60ul, 27}, // 11111111|11111111|11111100|011 + {222, 0xfffffc80ul, 27}, // 11111111|11111111|11111100|100 + {223, 0xfffffca0ul, 27}, // 11111111|11111111|11111100|101 + {224, 0xfffec000ul, 20}, // 11111111|11111110|1100 + {225, 0xfffff300ul, 24}, // 11111111|11111111|11110011 + {226, 0xfffed000ul, 20}, // 11111111|11111110|1101 + {227, 0xffff3000ul, 21}, // 11111111|11111111|00110 + {228, 0xffffa400ul, 22}, // 11111111|11111111|101001 + {229, 0xffff3800ul, 21}, // 11111111|11111111|00111 + {230, 0xffff4000ul, 21}, // 11111111|11111111|01000 + {231, 0xffffe600ul, 23}, // 11111111|11111111|1110011 + {232, 0xffffa800ul, 22}, // 11111111|11111111|101010 + {233, 0xffffac00ul, 22}, // 11111111|11111111|101011 + {234, 0xfffff700ul, 25}, // 11111111|11111111|11110111|0 + {235, 0xfffff780ul, 25}, // 11111111|11111111|11110111|1 + {236, 0xfffff400ul, 24}, // 11111111|11111111|11110100 + {237, 0xfffff500ul, 24}, // 11111111|11111111|11110101 + {238, 0xfffffa80ul, 26}, // 11111111|11111111|11111010|10 + {239, 0xffffe800ul, 23}, // 11111111|11111111|1110100 + {240, 0xfffffac0ul, 26}, // 11111111|11111111|11111010|11 + {241, 0xfffffcc0ul, 27}, // 11111111|11111111|11111100|110 + {242, 0xfffffb00ul, 26}, // 11111111|11111111|11111011|00 + {243, 0xfffffb40ul, 26}, // 11111111|11111111|11111011|01 + {244, 0xfffffce0ul, 27}, // 11111111|11111111|11111100|111 + {245, 0xfffffd00ul, 27}, // 11111111|11111111|11111101|000 + {246, 0xfffffd20ul, 27}, // 11111111|11111111|11111101|001 + {247, 0xfffffd40ul, 27}, // 11111111|11111111|11111101|010 + {248, 0xfffffd60ul, 27}, // 11111111|11111111|11111101|011 + {249, 0xffffffe0ul, 28}, // 11111111|11111111|11111111|1110 + {250, 0xfffffd80ul, 27}, // 11111111|11111111|11111101|100 + {251, 0xfffffda0ul, 27}, // 11111111|11111111|11111101|101 + {252, 0xfffffdc0ul, 27}, // 11111111|11111111|11111101|110 + {253, 0xfffffde0ul, 27}, // 11111111|11111111|11111101|111 + {254, 0xfffffe00ul, 27}, // 11111111|11111111|11111110|000 + {255, 0xfffffb80ul, 26}, // 11111111|11111111|11111011|10 + {256, 0xfffffffcul, 30} // EOS 11111111|11111111|11111111|111111 +}; + +void write_huffman_code(BitOStream &outputStream, const CodeEntry &code) +{ + // Append octet by octet. + auto bitLength = code.bitLength; + const auto hc = code.huffmanCode >> (32 - bitLength); + + if (bitLength > 24) { + outputStream.writeBits(uchar(hc >> 24), bitLength - 24); + bitLength = 24; + } + + if (bitLength > 16) { + outputStream.writeBits(uchar(hc >> 16), bitLength - 16); + bitLength = 16; + } + + if (bitLength > 8) { + outputStream.writeBits(uchar(hc >> 8), bitLength - 8); + bitLength = 8; + } + + outputStream.writeBits(uchar(hc), bitLength); +} + +} + +// That's from HPACK's specs - we deal with octets. +static_assert(std::numeric_limits<uchar>::digits == 8, "octets expected"); + +quint64 huffman_encoded_bit_length(const QByteArray &inputData) +{ + quint64 bitLength = 0; + for (int i = 0, e = inputData.size(); i < e; ++i) + bitLength += staticHuffmanCodeTable[int(inputData[i])].bitLength; + + return bitLength; +} + +void huffman_encode_string(const QByteArray &inputData, BitOStream &outputStream) +{ + for (int i = 0, e = inputData.size(); i < e; ++i) + write_huffman_code(outputStream, staticHuffmanCodeTable[int(inputData[i])]); + + // Pad bits ... + if (outputStream.bitLength() % 8) + outputStream.writeBits(0xff, 8 - outputStream.bitLength() % 8); +} + +bool padding_is_valid(quint32 chunk, quint32 nBits) +{ + Q_ASSERT(nBits); + + // HPACK, 5.2: "A padding strictly longer than 7 bits MUST be + // treated as a decoding error." + if (nBits > 7) + return false; + // HPACK, 5.2: + // "A padding not corresponding to the most significant bits + // of the code for the EOS symbol MUST be treated as a decoding error." + return (chunk >> (32 - nBits)) == quint32((1 << nBits) - 1); +} + +HuffmanDecoder::HuffmanDecoder() + : minCodeLength() +{ + const auto nCodes = sizeof staticHuffmanCodeTable / sizeof staticHuffmanCodeTable[0]; + + std::vector<CodeEntry> symbols(staticHuffmanCodeTable, staticHuffmanCodeTable + nCodes); + // Now we sort: by bit length first (in the descending order) and by the symbol + // value (descending). Descending order: to make sure we do not create prefix tables with + // short 'indexLength' first and having longer codes that do not fit into such tables later. + std::sort(symbols.begin(), symbols.end(), [](const CodeEntry &code1, const CodeEntry &code2) { + if (code1.bitLength == code2.bitLength) + return code1.byteValue > code2.byteValue; + return code1.bitLength > code2.bitLength; + }); + + minCodeLength = symbols.back().bitLength; // The shortest one, currently it's 5. + + // TODO: add a verification - Huffman codes + // within a given bit length range also + // should be in descending order. + + // Initialize 'prefixTables' and 'tableData'. + addTable(0, quint32(BitConstants::rootPrefix)); // 'root' table. + + for (const auto &s : symbols) { + quint32 tableIndex = 0; + while (true) { + Q_ASSERT(tableIndex < prefixTables.size()); + // Note, by value - since prefixTables will be updated in between. + const auto table = prefixTables[tableIndex]; + // We skip prefixed bits (if any) and use indexed bits only: + const auto entryIndex = s.huffmanCode << table.prefixLength >> (32 - table.indexLength); + // Again, by value. + PrefixTableEntry entry = tableEntry(table, entryIndex); + // How many bits were coded by previous tables and this table: + const auto codedLength = table.prefixLength + table.indexLength; + if (codedLength < s.bitLength) { + // We have to add a new prefix table ... (if it's not done yet). + if (!entry.bitLength) { + entry.nextTable = addTable(codedLength, std::min<quint32>(quint32(BitConstants::childPrefix), + s.bitLength - codedLength)); + entry.bitLength = s.bitLength; + entry.byteValue = s.byteValue; + setTableEntry(table, entryIndex, entry); + } + tableIndex = entry.nextTable; + } else { + // We found the slot for our code (terminal entry): + entry.byteValue = s.byteValue; + entry.bitLength = s.bitLength; + // Refer to our own table as 'nextTable': + entry.nextTable = tableIndex; + setTableEntry(table, entryIndex, entry); + break; + } + } + } + + // Now, we have a table(s) and have to fill 'holes' to + // 'fix' terminal entries. + for (const auto &table : prefixTables) { + const quint32 codedLength = table.prefixLength + table.indexLength; + for (quint32 j = 0; j < table.size();) { + const PrefixTableEntry &entry = tableEntry(table, j); + if (entry.bitLength && entry.bitLength < codedLength) { + const quint32 range = 1 << (codedLength - entry.bitLength); + for (quint32 k = 1; k < range; ++k) + setTableEntry(table, j + k, entry); + j += range; + } else { + ++j; + } + } + } +} + +bool HuffmanDecoder::decodeStream(BitIStream &inputStream, QByteArray &outputBuffer) +{ + while (true) { + quint32 chunk = 0; + const quint32 readBits = inputStream.peekBits(inputStream.streamOffset(), 32, &chunk); + if (!readBits) + return !inputStream.hasMoreBits(); + + if (readBits < minCodeLength) { + inputStream.skipBits(readBits); + return padding_is_valid(chunk, readBits); + } + + quint32 tableIndex = 0; + const PrefixTable *table = &prefixTables[tableIndex]; + quint32 entryIndex = chunk >> (32 - table->indexLength); + PrefixTableEntry entry = tableEntry(*table, entryIndex); + + while (true) { + if (entry.nextTable == tableIndex) + break; + + tableIndex = entry.nextTable; + table = &prefixTables[tableIndex]; + entryIndex = chunk << table->prefixLength >> (32 - table->indexLength); + entry = tableEntry(*table, entryIndex); + } + + if (entry.bitLength > readBits) { + inputStream.skipBits(readBits); + return padding_is_valid(chunk, readBits); + } + + if (!entry.bitLength || entry.byteValue == 256) { + //EOS (256) == compression error (HPACK). + inputStream.skipBits(readBits); + return false; + } + + outputBuffer.append(entry.byteValue); + inputStream.skipBits(entry.bitLength); + } + + return false; +} + +quint32 HuffmanDecoder::addTable(quint32 prefix, quint32 index) +{ + PrefixTable newTable{prefix, index}; + newTable.offset = quint32(tableData.size()); + prefixTables.push_back(newTable); + // Add entries for this table: + tableData.resize(tableData.size() + newTable.size()); + + return quint32(prefixTables.size() - 1); +} + +PrefixTableEntry HuffmanDecoder::tableEntry(const PrefixTable &table, quint32 index) +{ + Q_ASSERT(index < table.size()); + return tableData[table.offset + index]; +} + +void HuffmanDecoder::setTableEntry(const PrefixTable &table, quint32 index, + const PrefixTableEntry &entry) +{ + Q_ASSERT(index < table.size()); + tableData[table.offset + index] = entry; +} + +bool huffman_decode_string(BitIStream &inputStream, QByteArray *outputBuffer) +{ + Q_ASSERT(outputBuffer); + + static HuffmanDecoder decoder; + return decoder.decodeStream(inputStream, *outputBuffer); +} + +} + +QT_END_NAMESPACE diff --git a/src/network/access/http2/huffman_p.h b/src/network/access/http2/huffman_p.h new file mode 100644 index 0000000000..7195661664 --- /dev/null +++ b/src/network/access/http2/huffman_p.h @@ -0,0 +1,182 @@ +/**************************************************************************** +** +** 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 HUFFMAN_P_H +#define HUFFMAN_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qglobal.h> + +QT_BEGIN_NAMESPACE + +class QByteArray; + +namespace HPack +{ + +struct CodeEntry +{ + CodeEntry() : byteValue(), + huffmanCode(), + bitLength() + { + } + + CodeEntry(quint32 val, quint32 code, quint32 len) + : byteValue(val), + huffmanCode(code), + bitLength(len) + { + } + + quint32 byteValue; + quint32 huffmanCode; + quint32 bitLength; +}; + +class BitOStream; + +quint64 huffman_encoded_bit_length(const QByteArray &inputData); +void huffman_encode_string(const QByteArray &inputData, BitOStream &outputStream); + +// PrefixTable: +// Huffman codes with a small bit length +// fit into a table (these are 'terminal' symbols), +// codes with longer codes require additional +// tables, so several symbols will have the same index +// in a table - pointing into the next table. +// Every table has an 'indexLength' - that's +// how many bits can fit in table's indices + +// 'prefixLength' - how many bits were addressed +// by its 'parent' table(s). +// All PrefixTables are kept in 'prefixTables' array. +// PrefixTable itself does not have any entries, +// it just holds table's prefix/index + 'offset' - +// there table's data starts in an array of all +// possible entries ('tableData'). + +struct PrefixTable +{ + PrefixTable() + : prefixLength(), + indexLength(), + offset() + { + } + + PrefixTable(quint32 prefix, quint32 index) + : prefixLength(prefix), + indexLength(index), + offset() + { + } + + quint32 size()const + { + // Number of entries table contains: + return 1 << indexLength; + } + + quint32 prefixLength; + quint32 indexLength; + quint32 offset; +}; + +// Table entry is either a terminal entry (thus probably the code found) +// or points into another table ('nextTable' - index into +// 'prefixTables' array). If it's a terminal, 'nextTable' index +// refers to the same table. + +struct PrefixTableEntry +{ + PrefixTableEntry() + : bitLength(), + nextTable(), + byteValue() + { + } + + quint32 bitLength; + quint32 nextTable; + quint32 byteValue; +}; + +class BitIStream; + +class HuffmanDecoder +{ +public: + enum class BitConstants + { + rootPrefix = 9, + childPrefix = 6 + }; + + HuffmanDecoder(); + + bool decodeStream(BitIStream &inputStream, QByteArray &outputBuffer); + +private: + quint32 addTable(quint32 prefixLength, quint32 indexLength); + PrefixTableEntry tableEntry(const PrefixTable &table, quint32 index); + void setTableEntry(const PrefixTable &table, quint32 index, const PrefixTableEntry &entry); + + std::vector<PrefixTable> prefixTables; + std::vector<PrefixTableEntry> tableData; + quint32 minCodeLength; +}; + +bool huffman_decode_string(BitIStream &inputStream, QByteArray *outputBuffer); + +} // namespace HPack + +QT_END_NAMESPACE + +#endif + diff --git a/src/network/access/qabstractnetworkcache.h b/src/network/access/qabstractnetworkcache.h index 6b80147757..33b0bc4ce3 100644 --- a/src/network/access/qabstractnetworkcache.h +++ b/src/network/access/qabstractnetworkcache.h @@ -40,6 +40,7 @@ #ifndef QABSTRACTNETWORKCACHE_H #define QABSTRACTNETWORKCACHE_H +#include <QtNetwork/qtnetworkglobal.h> #include <QtCore/qobject.h> #include <QtCore/qshareddata.h> #include <QtCore/qpair.h> diff --git a/src/network/access/qabstractnetworkcache_p.h b/src/network/access/qabstractnetworkcache_p.h index 7be89f22b6..fee723e315 100644 --- a/src/network/access/qabstractnetworkcache_p.h +++ b/src/network/access/qabstractnetworkcache_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "private/qobject_p.h" QT_BEGIN_NAMESPACE diff --git a/src/network/access/qabstractprotocolhandler_p.h b/src/network/access/qabstractprotocolhandler_p.h index a05df3e429..30814d6737 100644 --- a/src/network/access/qabstractprotocolhandler_p.h +++ b/src/network/access/qabstractprotocolhandler_p.h @@ -51,9 +51,9 @@ // We mean it. // -#ifndef QT_NO_HTTP +#include <QtNetwork/private/qtnetworkglobal_p.h> -#include <QtCore/qglobal.h> +#ifndef QT_NO_HTTP QT_BEGIN_NAMESPACE diff --git a/src/network/access/qftp_p.h b/src/network/access/qftp_p.h index 37f8f2f8d2..6cf5116798 100644 --- a/src/network/access/qftp_p.h +++ b/src/network/access/qftp_p.h @@ -48,9 +48,10 @@ // We mean it. // -#ifndef QFTP_H -#define QFTP_H +#ifndef QFTP_P_H +#define QFTP_P_H +#include <QtNetwork/private/qtnetworkglobal_p.h> #include <QtCore/qstring.h> #include <private/qurlinfo_p.h> #include <QtCore/qobject.h> @@ -172,4 +173,4 @@ private: QT_END_NAMESPACE -#endif // QFTP_H +#endif // QFTP_P_H diff --git a/src/network/access/qhttp2protocolhandler.cpp b/src/network/access/qhttp2protocolhandler.cpp new file mode 100644 index 0000000000..937686920c --- /dev/null +++ b/src/network/access/qhttp2protocolhandler.cpp @@ -0,0 +1,1217 @@ +/**************************************************************************** +** +** 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 "qhttpnetworkconnection_p.h" +#include "qhttp2protocolhandler_p.h" + +#if !defined(QT_NO_HTTP) && !defined(QT_NO_SSL) + +#include "http2/bitstreams_p.h" + +#include <private/qnoncontiguousbytedevice_p.h> + +#include <QtNetwork/qabstractsocket.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qendian.h> +#include <QtCore/qdebug.h> +#include <QtCore/qlist.h> +#include <QtCore/qurl.h> + +#include <algorithm> +#include <vector> + +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<quint32>::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<qint32>::max() - windowSize < delta; + return std::numeric_limits<qint32>::min() - windowSize > delta; +} + +}// Unnamed namespace + +// Since we anyway end up having this in every function definition: +using namespace Http2; + +const std::deque<quint32>::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<QNonContiguousByteDevice *>(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<quint32>(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<qint32>(sessionSendWindowSize, stream.sendWindow); + while (!stream.data()->atEnd() && slot) { + qint64 chunkSize = 0; + const uchar *src = + reinterpret_cast<const uchar *>(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<qint32>(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<quint32>(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<quint16>(src)); + const quint32 intVal = qFromBigEndian<quint32>(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<quint32>(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<quint32>(src); + const quint32 errorCode = qFromBigEndian<quint32>(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<quint32>(inboundFrame.dataBegin()); + const bool valid = delta && delta <= quint32(std::numeric_limits<qint32>::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<uchar> 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<qint32>::max())) { + connectionError(FLOW_CONTROL_ERROR, "SETTINGS invalid initial window size"); + return false; + } + + const qint32 delta = qint32(newValue) - streamInitialSendWindowSize; + streamInitialSendWindowSize = newValue; + + std::vector<quint32> 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<const char *>(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<qint32>::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(); +} + +QT_END_NAMESPACE + +#endif // !defined(QT_NO_HTTP) && !defined(QT_NO_SSL) diff --git a/src/network/access/qhttp2protocolhandler_p.h b/src/network/access/qhttp2protocolhandler_p.h new file mode 100644 index 0000000000..b146e37dd3 --- /dev/null +++ b/src/network/access/qhttp2protocolhandler_p.h @@ -0,0 +1,207 @@ +/**************************************************************************** +** +** 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 + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the Network Access API. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qhttpnetworkconnectionchannel_p.h> +#include <private/qabstractprotocolhandler_p.h> +#include <private/qhttpnetworkrequest_p.h> + +#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 <QtCore/qbytearray.h> +#include <QtCore/qglobal.h> +#include <QtCore/qobject.h> +#include <QtCore/qflags.h> +#include <QtCore/qhash.h> + +#include <vector> +#include <limits> +#include <deque> +#include <set> + +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<quint32, Stream> activeStreams; + std::deque<quint32> suspendedStreams[3]; // 3 for priorities: High, Normal, Low. + static const std::deque<quint32>::size_type maxRecycledStreams; + std::deque<quint32> 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<Http2::FrameReader> 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<quint32>::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/qhttpmultipart.h b/src/network/access/qhttpmultipart.h index cbf28c083e..6d4531b099 100644 --- a/src/network/access/qhttpmultipart.h +++ b/src/network/access/qhttpmultipart.h @@ -40,6 +40,7 @@ #ifndef QHTTPMULTIPART_H #define QHTTPMULTIPART_H +#include <QtNetwork/qtnetworkglobal.h> #include <QtCore/QSharedDataPointer> #include <QtCore/QByteArray> #include <QtCore/QIODevice> diff --git a/src/network/access/qhttpmultipart_p.h b/src/network/access/qhttpmultipart_p.h index 875f52f8e3..a03df9cb13 100644 --- a/src/network/access/qhttpmultipart_p.h +++ b/src/network/access/qhttpmultipart_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "QtCore/qshareddata.h" #include "qnetworkrequest_p.h" // for deriving QHttpPartPrivate from QNetworkHeadersPrivate #include "private/qobject_p.h" diff --git a/src/network/access/qhttpnetworkconnection.cpp b/src/network/access/qhttpnetworkconnection.cpp index 79f418f675..09cea8e769 100644 --- a/src/network/access/qhttpnetworkconnection.cpp +++ b/src/network/access/qhttpnetworkconnection.cpp @@ -60,6 +60,7 @@ # include <QtNetwork/qsslkey.h> # include <QtNetwork/qsslcipher.h> # include <QtNetwork/qsslconfiguration.h> +# include <QtNetwork/qsslerror.h> #endif @@ -82,7 +83,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 +1013,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 e05bc1df74..cad0ab1ac4 100644 --- a/src/network/access/qhttpnetworkconnection_p.h +++ b/src/network/access/qhttpnetworkconnection_p.h @@ -50,6 +50,8 @@ // // We mean it. // + +#include <QtNetwork/private/qtnetworkglobal_p.h> #include <QtNetwork/qnetworkrequest.h> #include <QtNetwork/qnetworkreply.h> #include <QtNetwork/qabstractsocket.h> @@ -70,17 +72,6 @@ #ifndef QT_NO_HTTP -#ifndef QT_NO_SSL -#ifndef QT_NO_OPENSSL -# include <private/qsslcontext_openssl_p.h> -#endif -# include <private/qsslsocket_p.h> -# include <QtNetwork/qsslsocket.h> -# include <QtNetwork/qsslerror.h> -#else -# include <QtNetwork/qtcpsocket.h> -#endif - QT_BEGIN_NAMESPACE class QHttpNetworkRequest; @@ -88,6 +79,10 @@ class QHttpNetworkReply; class QHttpThreadDelegate; class QByteArray; class QHostInfo; +#ifndef QT_NO_SSL +class QSslConfiguration; +class QSslContext; +#endif // !QT_NO_SSL class QHttpNetworkConnectionPrivate; class Q_AUTOTEST_EXPORT QHttpNetworkConnection : public QObject @@ -97,7 +92,8 @@ public: enum ConnectionType { ConnectionTypeHTTP, - ConnectionTypeSPDY + ConnectionTypeSPDY, + ConnectionTypeHTTP2 }; #ifndef QT_NO_BEARERMANAGEMENT @@ -159,6 +155,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..3a780f636b 100644 --- a/src/network/access/qhttpnetworkconnectionchannel.cpp +++ b/src/network/access/qhttpnetworkconnectionchannel.cpp @@ -47,10 +47,12 @@ #ifndef QT_NO_HTTP +#include <private/qhttp2protocolhandler_p.h> #include <private/qhttpprotocolhandler_p.h> #include <private/qspdyprotocolhandler_p.h> #ifndef QT_NO_SSL +# include <private/qsslsocket_p.h> # include <QtNetwork/qsslkey.h> # include <QtNetwork/qsslcipher.h> # include <QtNetwork/qsslconfiguration.h> @@ -971,7 +973,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<HttpMessagePair> spdyPairs = spdyRequestsToSend.values(); for (int a = 0; a < spdyPairs.count(); ++a) { // emit error for all replies @@ -1003,7 +1006,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 +1047,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 +1074,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/qhttpnetworkconnectionchannel_p.h b/src/network/access/qhttpnetworkconnectionchannel_p.h index b3b52a7f42..d7d5d86a7a 100644 --- a/src/network/access/qhttpnetworkconnectionchannel_p.h +++ b/src/network/access/qhttpnetworkconnectionchannel_p.h @@ -50,6 +50,8 @@ // // We mean it. // + +#include <QtNetwork/private/qtnetworkglobal_p.h> #include <QtNetwork/qnetworkrequest.h> #include <QtNetwork/qnetworkreply.h> #include <QtNetwork/qabstractsocket.h> diff --git a/src/network/access/qhttpnetworkheader_p.h b/src/network/access/qhttpnetworkheader_p.h index 3eaab587a0..89169b9331 100644 --- a/src/network/access/qhttpnetworkheader_p.h +++ b/src/network/access/qhttpnetworkheader_p.h @@ -50,6 +50,9 @@ // // We mean it. // + +#include <QtNetwork/private/qtnetworkglobal_p.h> + #ifndef QT_NO_HTTP #include <qshareddata.h> diff --git a/src/network/access/qhttpnetworkreply.cpp b/src/network/access/qhttpnetworkreply.cpp index 3601c36bc2..24ada3a81f 100644 --- a/src/network/access/qhttpnetworkreply.cpp +++ b/src/network/access/qhttpnetworkreply.cpp @@ -484,8 +484,7 @@ qint64 QHttpNetworkReplyPrivate::readStatus(QAbstractSocket *socket) } // is this a valid reply? - if (fragment.length() >= 5 && !fragment.startsWith("HTTP/")) - { + if (fragment.length() == 5 && !fragment.startsWith("HTTP/")) { fragment.clear(); return -1; } @@ -739,6 +738,8 @@ qint64 QHttpNetworkReplyPrivate::readBody(QAbstractSocket *socket, QByteDataBuff #ifndef QT_NO_COMPRESS int QHttpNetworkReplyPrivate::initializeInflateStream() { + Q_ASSERT(inflateStrm); + inflateStrm->zalloc = Z_NULL; inflateStrm->zfree = Z_NULL; inflateStrm->opaque = Z_NULL; diff --git a/src/network/access/qhttpnetworkreply_p.h b/src/network/access/qhttpnetworkreply_p.h index da39633dd1..f3b007f594 100644 --- a/src/network/access/qhttpnetworkreply_p.h +++ b/src/network/access/qhttpnetworkreply_p.h @@ -50,9 +50,13 @@ // // We mean it. // -#include <qplatformdefs.h> + +#include <QtNetwork/private/qtnetworkglobal_p.h> + #ifndef QT_NO_HTTP +#include <qplatformdefs.h> + #ifndef QT_NO_COMPRESS struct z_stream_s; #endif @@ -176,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 d9540e6369..d1abb76e28 100644 --- a/src/network/access/qhttpnetworkrequest_p.h +++ b/src/network/access/qhttpnetworkrequest_p.h @@ -50,6 +50,8 @@ // // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> + #ifndef QT_NO_HTTP #include <private/qhttpnetworkheader_p.h> @@ -115,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); @@ -142,6 +147,7 @@ private: friend class QHttpNetworkConnectionPrivate; friend class QHttpNetworkConnectionChannel; friend class QHttpProtocolHandler; + friend class QHttp2ProtocolHandler; friend class QSpdyProtocolHandler; }; @@ -163,6 +169,7 @@ public: bool autoDecompress; bool pipeliningAllowed; bool spdyAllowed; + bool http2Allowed; bool withCredentials; bool ssl; bool preConnect; diff --git a/src/network/access/qhttpprotocolhandler_p.h b/src/network/access/qhttpprotocolhandler_p.h index b13993778c..863b988be3 100644 --- a/src/network/access/qhttpprotocolhandler_p.h +++ b/src/network/access/qhttpprotocolhandler_p.h @@ -52,6 +52,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include <private/qabstractprotocolhandler_p.h> #ifndef QT_NO_HTTP 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<QByteArray> 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<QByteArray> nextProtocols; diff --git a/src/network/access/qhttpthreaddelegate_p.h b/src/network/access/qhttpthreaddelegate_p.h index cec125d7a5..64c58cf648 100644 --- a/src/network/access/qhttpthreaddelegate_p.h +++ b/src/network/access/qhttpthreaddelegate_p.h @@ -52,6 +52,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include <QObject> #include <QThreadStorage> #include <QNetworkProxy> diff --git a/src/network/access/qnetworkaccessauthenticationmanager_p.h b/src/network/access/qnetworkaccessauthenticationmanager_p.h index 3d1cd6b4fb..548675728f 100644 --- a/src/network/access/qnetworkaccessauthenticationmanager_p.h +++ b/src/network/access/qnetworkaccessauthenticationmanager_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "qnetworkaccessmanager.h" #include "qnetworkaccesscache_p.h" #include "qnetworkaccessbackend_p.h" diff --git a/src/network/access/qnetworkaccessbackend_p.h b/src/network/access/qnetworkaccessbackend_p.h index 47f5872235..7f39c942a3 100644 --- a/src/network/access/qnetworkaccessbackend_p.h +++ b/src/network/access/qnetworkaccessbackend_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "qnetworkreplyimpl_p.h" #include "QtCore/qobject.h" diff --git a/src/network/access/qnetworkaccesscache_p.h b/src/network/access/qnetworkaccesscache_p.h index 2337dd10af..3732b5cbb4 100644 --- a/src/network/access/qnetworkaccesscache_p.h +++ b/src/network/access/qnetworkaccesscache_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "QtCore/qobject.h" #include "QtCore/qbasictimer.h" #include "QtCore/qbytearray.h" diff --git a/src/network/access/qnetworkaccesscachebackend_p.h b/src/network/access/qnetworkaccesscachebackend_p.h index c9b8c84579..8db1a6b1d5 100644 --- a/src/network/access/qnetworkaccesscachebackend_p.h +++ b/src/network/access/qnetworkaccesscachebackend_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "qnetworkaccessbackend_p.h" #include "qnetworkrequest.h" #include "qnetworkreply.h" diff --git a/src/network/access/qnetworkaccessdebugpipebackend_p.h b/src/network/access/qnetworkaccessdebugpipebackend_p.h index 6f8e248880..1d1af61dbd 100644 --- a/src/network/access/qnetworkaccessdebugpipebackend_p.h +++ b/src/network/access/qnetworkaccessdebugpipebackend_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "qnetworkaccessbackend_p.h" #include "qnetworkrequest.h" #include "qnetworkreply.h" diff --git a/src/network/access/qnetworkaccessfilebackend_p.h b/src/network/access/qnetworkaccessfilebackend_p.h index dec90062e4..081ff2b9e7 100644 --- a/src/network/access/qnetworkaccessfilebackend_p.h +++ b/src/network/access/qnetworkaccessfilebackend_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "qnetworkaccessbackend_p.h" #include "qnetworkrequest.h" #include "qnetworkreply.h" diff --git a/src/network/access/qnetworkaccessftpbackend_p.h b/src/network/access/qnetworkaccessftpbackend_p.h index c5beaf94e8..cdf6b7a07f 100644 --- a/src/network/access/qnetworkaccessftpbackend_p.h +++ b/src/network/access/qnetworkaccessftpbackend_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "qnetworkaccessbackend_p.h" #include "qnetworkaccesscache_p.h" #include "qnetworkrequest.h" diff --git a/src/network/access/qnetworkaccessmanager.cpp b/src/network/access/qnetworkaccessmanager.cpp index 587ab27a0f..246b9a7bad 100644 --- a/src/network/access/qnetworkaccessmanager.cpp +++ b/src/network/access/qnetworkaccessmanager.cpp @@ -1105,6 +1105,47 @@ QNetworkReply *QNetworkAccessManager::sendCustomRequest(const QNetworkRequest &r } /*! + \since 5.8 + + \overload + + Sends the contents of the \a data byte array to the destination + specified by \a request. +*/ +QNetworkReply *QNetworkAccessManager::sendCustomRequest(const QNetworkRequest &request, const QByteArray &verb, const QByteArray &data) +{ + QBuffer *buffer = new QBuffer; + buffer->setData(data); + buffer->open(QIODevice::ReadOnly); + + QNetworkReply *reply = sendCustomRequest(request, verb, buffer); + buffer->setParent(reply); + return reply; +} + +/*! + \since 5.8 + + \overload + + Sends a custom request to the server identified by the URL of \a request. + + Sends the contents of the \a multiPart message to the destination + specified by \a request. + + This can be used for sending MIME multipart messages for custom verbs. + + \sa QHttpMultiPart, QHttpPart, put() +*/ +QNetworkReply *QNetworkAccessManager::sendCustomRequest(const QNetworkRequest &request, const QByteArray &verb, QHttpMultiPart *multiPart) +{ + QNetworkRequest newRequest = d_func()->prepareMultipart(request, multiPart); + QIODevice *device = multiPart->d_func()->device; + QNetworkReply *reply = sendCustomRequest(newRequest, verb, device); + return reply; +} + +/*! Returns a new QNetworkReply object to handle the operation \a op and request \a req. The device \a outgoingData is always 0 for Get and Head requests, but is the value passed to post() and put() in @@ -1516,27 +1557,35 @@ void QNetworkAccessManagerPrivate::clearCache(QNetworkAccessManager *manager) manager->d_func()->objectCache.clear(); manager->d_func()->authenticationManager->clearCache(); - if (manager->d_func()->httpThread) { - manager->d_func()->httpThread->quit(); - manager->d_func()->httpThread->wait(5000); - if (manager->d_func()->httpThread->isFinished()) - delete manager->d_func()->httpThread; - else - QObject::connect(manager->d_func()->httpThread, SIGNAL(finished()), manager->d_func()->httpThread, SLOT(deleteLater())); - manager->d_func()->httpThread = 0; - } + manager->d_func()->destroyThread(); } QNetworkAccessManagerPrivate::~QNetworkAccessManagerPrivate() { - if (httpThread) { - httpThread->quit(); - httpThread->wait(5000); - if (httpThread->isFinished()) - delete httpThread; + destroyThread(); +} + +QThread * QNetworkAccessManagerPrivate::createThread() +{ + if (!thread) { + thread = new QThread; + thread->setObjectName(QStringLiteral("QNetworkAccessManager thread")); + thread->start(); + } + Q_ASSERT(thread); + return thread; +} + +void QNetworkAccessManagerPrivate::destroyThread() +{ + if (thread) { + thread->quit(); + thread->wait(5000); + if (thread->isFinished()) + delete thread; else - QObject::connect(httpThread, SIGNAL(finished()), httpThread, SLOT(deleteLater())); - httpThread = 0; + QObject::connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); + thread = 0; } } diff --git a/src/network/access/qnetworkaccessmanager.h b/src/network/access/qnetworkaccessmanager.h index cba5cc09d6..4b8c4ddf0e 100644 --- a/src/network/access/qnetworkaccessmanager.h +++ b/src/network/access/qnetworkaccessmanager.h @@ -40,6 +40,7 @@ #ifndef QNETWORKACCESSMANAGER_H #define QNETWORKACCESSMANAGER_H +#include <QtNetwork/qtnetworkglobal.h> #include <QtCore/QObject> #ifndef QT_NO_SSL #include <QtNetwork/QSslConfiguration> @@ -128,6 +129,8 @@ public: QNetworkReply *put(const QNetworkRequest &request, QHttpMultiPart *multiPart); QNetworkReply *deleteResource(const QNetworkRequest &request); QNetworkReply *sendCustomRequest(const QNetworkRequest &request, const QByteArray &verb, QIODevice *data = Q_NULLPTR); + QNetworkReply *sendCustomRequest(const QNetworkRequest &request, const QByteArray &verb, const QByteArray &data); + QNetworkReply *sendCustomRequest(const QNetworkRequest &request, const QByteArray &verb, QHttpMultiPart *multiPart); #ifndef QT_NO_BEARERMANAGEMENT void setConfiguration(const QNetworkConfiguration &config); @@ -173,6 +176,7 @@ private: friend class QNetworkReplyImplPrivate; friend class QNetworkReplyHttpImpl; friend class QNetworkReplyHttpImplPrivate; + friend class QNetworkReplyFileImpl; Q_DECLARE_PRIVATE(QNetworkAccessManager) Q_PRIVATE_SLOT(d_func(), void _q_replyFinished()) diff --git a/src/network/access/qnetworkaccessmanager_p.h b/src/network/access/qnetworkaccessmanager_p.h index 44cf253368..bb4641ab8b 100644 --- a/src/network/access/qnetworkaccessmanager_p.h +++ b/src/network/access/qnetworkaccessmanager_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "qnetworkaccessmanager.h" #include "qnetworkaccesscache_p.h" #include "qnetworkaccessbackend_p.h" @@ -74,7 +75,7 @@ class QNetworkAccessManagerPrivate: public QObjectPrivate public: QNetworkAccessManagerPrivate() : networkCache(0), cookieJar(0), - httpThread(0), + thread(0), #ifndef QT_NO_NETWORKPROXY proxyFactory(0), #endif @@ -107,6 +108,9 @@ public: } ~QNetworkAccessManagerPrivate(); + QThread * createThread(); + void destroyThread(); + void _q_replyFinished(); void _q_replyEncrypted(); void _q_replySslErrors(const QList<QSslError> &errors); @@ -163,7 +167,7 @@ public: QNetworkCookieJar *cookieJar; - QThread *httpThread; + QThread *thread; #ifndef QT_NO_NETWORKPROXY diff --git a/src/network/access/qnetworkcookie.cpp b/src/network/access/qnetworkcookie.cpp index 21dc12829a..7a538cbf08 100644 --- a/src/network/access/qnetworkcookie.cpp +++ b/src/network/access/qnetworkcookie.cpp @@ -970,7 +970,7 @@ QList<QNetworkCookie> QNetworkCookiePrivate::parseSetCookieHeaderLine(const QByt if (ok) { if (secs <= 0) { //earliest representable time (RFC6265 section 5.2.2) - cookie.setExpirationDate(QDateTime::fromTime_t(0)); + cookie.setExpirationDate(QDateTime::fromSecsSinceEpoch(0)); } else { cookie.setExpirationDate(now.addSecs(secs)); } diff --git a/src/network/access/qnetworkcookie.h b/src/network/access/qnetworkcookie.h index 8f4b26465a..e462b98555 100644 --- a/src/network/access/qnetworkcookie.h +++ b/src/network/access/qnetworkcookie.h @@ -40,6 +40,7 @@ #ifndef QNETWORKCOOKIE_H #define QNETWORKCOOKIE_H +#include <QtNetwork/qtnetworkglobal.h> #include <QtCore/QSharedDataPointer> #include <QtCore/QList> #include <QtCore/QMetaType> diff --git a/src/network/access/qnetworkcookie_p.h b/src/network/access/qnetworkcookie_p.h index 61f04e7177..13538ad243 100644 --- a/src/network/access/qnetworkcookie_p.h +++ b/src/network/access/qnetworkcookie_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "QtCore/qdatetime.h" QT_BEGIN_NAMESPACE diff --git a/src/network/access/qnetworkcookiejar.h b/src/network/access/qnetworkcookiejar.h index d6370cbc05..f9c1549e20 100644 --- a/src/network/access/qnetworkcookiejar.h +++ b/src/network/access/qnetworkcookiejar.h @@ -40,6 +40,7 @@ #ifndef QNETWORKCOOKIEJAR_H #define QNETWORKCOOKIEJAR_H +#include <QtNetwork/qtnetworkglobal.h> #include <QtCore/QObject> #include <QtCore/QUrl> diff --git a/src/network/access/qnetworkcookiejar_p.h b/src/network/access/qnetworkcookiejar_p.h index b6f4eb2bc0..43f189a40c 100644 --- a/src/network/access/qnetworkcookiejar_p.h +++ b/src/network/access/qnetworkcookiejar_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "private/qobject_p.h" #include "qnetworkcookie.h" diff --git a/src/network/access/qnetworkdiskcache.cpp b/src/network/access/qnetworkdiskcache.cpp index 077826ccf6..ce3b773c64 100644 --- a/src/network/access/qnetworkdiskcache.cpp +++ b/src/network/access/qnetworkdiskcache.cpp @@ -422,7 +422,7 @@ QIODevice *QNetworkDiskCache::data(const QUrl &url) // ### verify that QFile uses the fd size and not the file name qint64 size = file->size() - file->pos(); const uchar *p = 0; -#if !defined(Q_OS_WINCE) && !defined(Q_OS_INTEGRITY) +#if !defined(Q_OS_INTEGRITY) p = file->map(file->pos(), size); #endif if (p) { diff --git a/src/network/access/qnetworkdiskcache.h b/src/network/access/qnetworkdiskcache.h index ad8b83a5f3..a3aa8d3a07 100644 --- a/src/network/access/qnetworkdiskcache.h +++ b/src/network/access/qnetworkdiskcache.h @@ -40,6 +40,7 @@ #ifndef QNETWORKDISKCACHE_H #define QNETWORKDISKCACHE_H +#include <QtNetwork/qtnetworkglobal.h> #include <QtNetwork/qabstractnetworkcache.h> QT_BEGIN_NAMESPACE diff --git a/src/network/access/qnetworkdiskcache_p.h b/src/network/access/qnetworkdiskcache_p.h index 9e035863dd..e47b93b09d 100644 --- a/src/network/access/qnetworkdiskcache_p.h +++ b/src/network/access/qnetworkdiskcache_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "private/qabstractnetworkcache_p.h" #include <qbuffer.h> diff --git a/src/network/access/qnetworkfile.cpp b/src/network/access/qnetworkfile.cpp new file mode 100644 index 0000000000..374dd26e2e --- /dev/null +++ b/src/network/access/qnetworkfile.cpp @@ -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$ +** +****************************************************************************/ + +#include "qnetworkfile_p.h" + +#include <QtCore/QDebug> +#include <QNetworkReply> +#include <QtCore/QDateTime> +#include <QtCore/QFileInfo> +#include <QtCore/QMetaObject> +#include <QtCore/QCoreApplication> + +QT_BEGIN_NAMESPACE + +QNetworkFile::QNetworkFile() + : QFile() +{ +} + +QNetworkFile::QNetworkFile(const QString &name) + : QFile(name) +{ +} + +void QNetworkFile::open() +{ + bool opened = false; + QFileInfo fi(fileName()); + if (fi.isDir()) { + QString msg = QCoreApplication::translate("QNetworkAccessFileBackend", + "Cannot open %1: Path is a directory").arg(fileName()); + error(QNetworkReply::ContentOperationNotPermittedError, msg); + } else { + headerRead(QNetworkRequest::LastModifiedHeader, QVariant::fromValue(fi.lastModified())); + headerRead(QNetworkRequest::ContentLengthHeader, QVariant::fromValue(fi.size())); + opened = QFile::open(QIODevice::ReadOnly | QIODevice::Unbuffered); + if (!opened) { + QString msg = QCoreApplication::translate("QNetworkAccessFileBackend", + "Error opening %1: %2").arg(fileName(), errorString()); + if (exists()) + error(QNetworkReply::ContentAccessDenied, msg); + else + error(QNetworkReply::ContentNotFoundError, msg); + } + } + finished(opened); +} + +void QNetworkFile::close() +{ + // This override is needed because 'using' keyword cannot be used for slots. And the base + // function is not an invokable/slot function. + QFile::close(); +} + +QT_END_NAMESPACE diff --git a/src/network/access/qnetworkfile_p.h b/src/network/access/qnetworkfile_p.h new file mode 100644 index 0000000000..dd56b24bd8 --- /dev/null +++ b/src/network/access/qnetworkfile_p.h @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** 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 QNETWORKFILE_H +#define QNETWORKFILE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the Network Access API. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include <QtNetwork/private/qtnetworkglobal_p.h> +#include <QFile> +#include <qnetworkreply.h> + +QT_BEGIN_NAMESPACE + +class QNetworkFile : public QFile +{ + Q_OBJECT +public: + QNetworkFile(); + QNetworkFile(const QString &name); + using QFile::open; + +public Q_SLOTS: + void open(); + void close() Q_DECL_OVERRIDE; + +Q_SIGNALS: + void finished(bool ok); + void headerRead(QNetworkRequest::KnownHeaders header, const QVariant &value); + void error(QNetworkReply::NetworkError error, const QString &message); +}; + +QT_END_NAMESPACE + +#endif // QNETWORKFILE_H diff --git a/src/network/access/qnetworkreply.h b/src/network/access/qnetworkreply.h index 9aaf06a803..1419db8597 100644 --- a/src/network/access/qnetworkreply.h +++ b/src/network/access/qnetworkreply.h @@ -40,6 +40,7 @@ #ifndef QNETWORKREPLY_H #define QNETWORKREPLY_H +#include <QtNetwork/qtnetworkglobal.h> #include <QtCore/QIODevice> #include <QtCore/QString> #include <QtCore/QVariant> diff --git a/src/network/access/qnetworkreply_p.h b/src/network/access/qnetworkreply_p.h index 420b862b17..66d8c9d527 100644 --- a/src/network/access/qnetworkreply_p.h +++ b/src/network/access/qnetworkreply_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "qnetworkrequest.h" #include "qnetworkrequest_p.h" #include "qnetworkreply.h" diff --git a/src/network/access/qnetworkreplydataimpl_p.h b/src/network/access/qnetworkreplydataimpl_p.h index 60d3f583cc..c8b44d7539 100644 --- a/src/network/access/qnetworkreplydataimpl_p.h +++ b/src/network/access/qnetworkreplydataimpl_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "qnetworkreply.h" #include "qnetworkreply_p.h" #include "qnetworkaccessmanager.h" diff --git a/src/network/access/qnetworkreplyfileimpl.cpp b/src/network/access/qnetworkreplyfileimpl.cpp index 36bc4b41df..ef319ebf0d 100644 --- a/src/network/access/qnetworkreplyfileimpl.cpp +++ b/src/network/access/qnetworkreplyfileimpl.cpp @@ -40,31 +40,45 @@ #include "qnetworkreplyfileimpl_p.h" #include "QtCore/qdatetime.h" +#include "qnetworkaccessmanager_p.h" #include <QtCore/QCoreApplication> #include <QtCore/QFileInfo> +#include <QtCore/QThread> +#include "qnetworkfile_p.h" +#include "qnetworkrequest.h" QT_BEGIN_NAMESPACE QNetworkReplyFileImplPrivate::QNetworkReplyFileImplPrivate() - : QNetworkReplyPrivate(), realFileSize(0) + : QNetworkReplyPrivate(), managerPrivate(0), realFile(0) { + qRegisterMetaType<QNetworkRequest::KnownHeaders>(); + qRegisterMetaType<QNetworkReply::NetworkError>(); } QNetworkReplyFileImpl::~QNetworkReplyFileImpl() { + QNetworkReplyFileImplPrivate *d = (QNetworkReplyFileImplPrivate*) d_func(); + if (d->realFile) { + if (d->realFile->thread() == QThread::currentThread()) + delete d->realFile; + else + QMetaObject::invokeMethod(d->realFile, "deleteLater", Qt::QueuedConnection); + } } -QNetworkReplyFileImpl::QNetworkReplyFileImpl(QObject *parent, const QNetworkRequest &req, const QNetworkAccessManager::Operation op) - : QNetworkReply(*new QNetworkReplyFileImplPrivate(), parent) +QNetworkReplyFileImpl::QNetworkReplyFileImpl(QNetworkAccessManager *manager, const QNetworkRequest &req, const QNetworkAccessManager::Operation op) + : QNetworkReply(*new QNetworkReplyFileImplPrivate(), manager) { setRequest(req); setUrl(req.url()); setOperation(op); - setFinished(true); QNetworkReply::open(QIODevice::ReadOnly); QNetworkReplyFileImplPrivate *d = (QNetworkReplyFileImplPrivate*) d_func(); + d->managerPrivate = manager->d_func(); + QUrl url = req.url(); if (url.host() == QLatin1String("localhost")) url.setHost(QString()); @@ -77,7 +91,7 @@ QNetworkReplyFileImpl::QNetworkReplyFileImpl(QObject *parent, const QNetworkRequ setError(QNetworkReply::ProtocolInvalidOperationError, msg); QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, Q_ARG(QNetworkReply::NetworkError, QNetworkReply::ProtocolInvalidOperationError)); - QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection); + fileOpenFinished(false); return; } #endif @@ -85,7 +99,6 @@ QNetworkReplyFileImpl::QNetworkReplyFileImpl(QObject *parent, const QNetworkRequ url.setPath(QLatin1String("/")); setUrl(url); - QString fileName = url.toLocalFile(); if (fileName.isEmpty()) { const QString scheme = url.scheme(); @@ -101,68 +114,85 @@ QNetworkReplyFileImpl::QNetworkReplyFileImpl(QObject *parent, const QNetworkRequ } } - QFileInfo fi(fileName); - if (fi.isDir()) { - QString msg = QCoreApplication::translate("QNetworkAccessFileBackend", "Cannot open %1: Path is a directory").arg(url.toString()); - setError(QNetworkReply::ContentOperationNotPermittedError, msg); - QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, - Q_ARG(QNetworkReply::NetworkError, QNetworkReply::ContentOperationNotPermittedError)); - QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection); - return; - } - - d->realFile.setFileName(fileName); - bool opened = d->realFile.open(QIODevice::ReadOnly | QIODevice::Unbuffered); - - // could we open the file? - if (!opened) { - QString msg = QCoreApplication::translate("QNetworkAccessFileBackend", "Error opening %1: %2") - .arg(d->realFile.fileName(), d->realFile.errorString()); - - if (d->realFile.exists()) { - setError(QNetworkReply::ContentAccessDenied, msg); - QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, - Q_ARG(QNetworkReply::NetworkError, QNetworkReply::ContentAccessDenied)); - } else { - setError(QNetworkReply::ContentNotFoundError, msg); + if (req.attribute(QNetworkRequest::BackgroundRequestAttribute).toBool()) { // Asynchronous open + auto realFile = new QNetworkFile(fileName); + connect(realFile, &QNetworkFile::headerRead, this, &QNetworkReplyFileImpl::setHeader, + Qt::QueuedConnection); + connect(realFile, &QNetworkFile::error, this, &QNetworkReplyFileImpl::setError, + Qt::QueuedConnection); + connect(realFile, SIGNAL(finished(bool)), SLOT(fileOpenFinished(bool)), + Qt::QueuedConnection); + + realFile->moveToThread(d->managerPrivate->createThread()); + QMetaObject::invokeMethod(realFile, "open", Qt::QueuedConnection); + + d->realFile = realFile; + } else { // Synch open + setFinished(true); + + QFileInfo fi(fileName); + if (fi.isDir()) { + QString msg = QCoreApplication::translate("QNetworkAccessFileBackend", "Cannot open %1: Path is a directory").arg(url.toString()); + setError(QNetworkReply::ContentOperationNotPermittedError, msg); QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, - Q_ARG(QNetworkReply::NetworkError, QNetworkReply::ContentNotFoundError)); + Q_ARG(QNetworkReply::NetworkError, QNetworkReply::ContentOperationNotPermittedError)); + QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection); + return; + } + d->realFile = new QFile(fileName, this); + bool opened = d->realFile->open(QIODevice::ReadOnly | QIODevice::Unbuffered); + + // could we open the file? + if (!opened) { + QString msg = QCoreApplication::translate("QNetworkAccessFileBackend", "Error opening %1: %2") + .arg(d->realFile->fileName(), d->realFile->errorString()); + + if (fi.exists()) { + setError(QNetworkReply::ContentAccessDenied, msg); + QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, + Q_ARG(QNetworkReply::NetworkError, QNetworkReply::ContentAccessDenied)); + } else { + setError(QNetworkReply::ContentNotFoundError, msg); + QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, + Q_ARG(QNetworkReply::NetworkError, QNetworkReply::ContentNotFoundError)); + } + QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection); + return; } + setHeader(QNetworkRequest::LastModifiedHeader, fi.lastModified()); + setHeader(QNetworkRequest::ContentLengthHeader, fi.size()); + + QMetaObject::invokeMethod(this, "metaDataChanged", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "downloadProgress", Qt::QueuedConnection, + Q_ARG(qint64, fi.size()), Q_ARG(qint64, fi.size())); + QMetaObject::invokeMethod(this, "readyRead", Qt::QueuedConnection); QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection); - return; } - - setHeader(QNetworkRequest::LastModifiedHeader, fi.lastModified()); - d->realFileSize = fi.size(); - setHeader(QNetworkRequest::ContentLengthHeader, d->realFileSize); - - QMetaObject::invokeMethod(this, "metaDataChanged", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "downloadProgress", Qt::QueuedConnection, - Q_ARG(qint64, d->realFileSize), Q_ARG(qint64, d->realFileSize)); - QMetaObject::invokeMethod(this, "readyRead", Qt::QueuedConnection); - QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection); } void QNetworkReplyFileImpl::close() { Q_D(QNetworkReplyFileImpl); QNetworkReply::close(); - d->realFile.close(); + if (d->realFile) { + if (d->realFile->thread() == thread()) + d->realFile->close(); + else + QMetaObject::invokeMethod(d->realFile, "close", Qt::QueuedConnection); + } } void QNetworkReplyFileImpl::abort() { - Q_D(QNetworkReplyFileImpl); - QNetworkReply::close(); - d->realFile.close(); + close(); } qint64 QNetworkReplyFileImpl::bytesAvailable() const { Q_D(const QNetworkReplyFileImpl); - if (!d->realFile.isOpen()) + if (!d->isFinished || !d->realFile || !d->realFile->isOpen()) return QNetworkReply::bytesAvailable(); - return QNetworkReply::bytesAvailable() + d->realFile.bytesAvailable(); + return QNetworkReply::bytesAvailable() + d->realFile->bytesAvailable(); } bool QNetworkReplyFileImpl::isSequential () const @@ -172,8 +202,9 @@ bool QNetworkReplyFileImpl::isSequential () const qint64 QNetworkReplyFileImpl::size() const { - Q_D(const QNetworkReplyFileImpl); - return d->realFileSize; + bool ok; + int size = header(QNetworkRequest::ContentLengthHeader).toInt(&ok); + return ok ? size : 0; } /*! @@ -182,11 +213,11 @@ qint64 QNetworkReplyFileImpl::size() const qint64 QNetworkReplyFileImpl::readData(char *data, qint64 maxlen) { Q_D(QNetworkReplyFileImpl); - if (!d->realFile.isOpen()) + if (!d->isFinished || !d->realFile || !d->realFile->isOpen()) return -1; - qint64 ret = d->realFile.read(data, maxlen); - if (bytesAvailable() == 0 && d->realFile.isOpen()) - d->realFile.close(); + qint64 ret = d->realFile->read(data, maxlen); + if (bytesAvailable() == 0) + d->realFile->close(); if (ret == 0 && bytesAvailable() == 0) return -1; else { @@ -196,6 +227,17 @@ qint64 QNetworkReplyFileImpl::readData(char *data, qint64 maxlen) } } +void QNetworkReplyFileImpl::fileOpenFinished(bool isOpen) +{ + setFinished(true); + if (isOpen) { + const auto fileSize = size(); + Q_EMIT metaDataChanged(); + Q_EMIT downloadProgress(fileSize, fileSize); + Q_EMIT readyRead(); + } + Q_EMIT finished(); +} QT_END_NAMESPACE diff --git a/src/network/access/qnetworkreplyfileimpl_p.h b/src/network/access/qnetworkreplyfileimpl_p.h index bac00881a8..d6af66152e 100644 --- a/src/network/access/qnetworkreplyfileimpl_p.h +++ b/src/network/access/qnetworkreplyfileimpl_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "qnetworkreply.h" #include "qnetworkreply_p.h" #include "qnetworkaccessmanager.h" @@ -59,13 +60,12 @@ QT_BEGIN_NAMESPACE - class QNetworkReplyFileImplPrivate; class QNetworkReplyFileImpl: public QNetworkReply { Q_OBJECT public: - QNetworkReplyFileImpl(QObject *parent, const QNetworkRequest &req, const QNetworkAccessManager::Operation op); + QNetworkReplyFileImpl(QNetworkAccessManager *manager, const QNetworkRequest &req, const QNetworkAccessManager::Operation op); ~QNetworkReplyFileImpl(); virtual void abort() Q_DECL_OVERRIDE; @@ -77,6 +77,9 @@ public: virtual qint64 readData(char *data, qint64 maxlen) Q_DECL_OVERRIDE; +private Q_SLOTS: + void fileOpenFinished(bool isOpen); + Q_DECLARE_PRIVATE(QNetworkReplyFileImpl) }; @@ -85,12 +88,14 @@ class QNetworkReplyFileImplPrivate: public QNetworkReplyPrivate public: QNetworkReplyFileImplPrivate(); - QFile realFile; - qint64 realFileSize; + QNetworkAccessManagerPrivate *managerPrivate; + QPointer<QFile> realFile; Q_DECLARE_PUBLIC(QNetworkReplyFileImpl) }; QT_END_NAMESPACE +Q_DECLARE_METATYPE(QNetworkRequest::KnownHeaders) + #endif // QNETWORKREPLYFILEIMPL_H diff --git a/src/network/access/qnetworkreplyhttpimpl.cpp b/src/network/access/qnetworkreplyhttpimpl.cpp index 6bf3c56db2..b5e44fa29a 100644 --- a/src/network/access/qnetworkreplyhttpimpl.cpp +++ b/src/network/access/qnetworkreplyhttpimpl.cpp @@ -522,36 +522,36 @@ bool QNetworkReplyHttpImplPrivate::loadFromCacheIfAllowed(QHttpNetworkRequest &h * now * is the current (local) time */ - int age_value = 0; + qint64 age_value = 0; it = cacheHeaders.findRawHeader("age"); if (it != cacheHeaders.rawHeaders.constEnd()) - age_value = it->second.toInt(); + age_value = it->second.toLongLong(); QDateTime dateHeader; - int date_value = 0; + qint64 date_value = 0; it = cacheHeaders.findRawHeader("date"); if (it != cacheHeaders.rawHeaders.constEnd()) { dateHeader = QNetworkHeadersPrivate::fromHttpDate(it->second); - date_value = dateHeader.toTime_t(); + date_value = dateHeader.toSecsSinceEpoch(); } - int now = currentDateTime.toTime_t(); - int request_time = now; - int response_time = now; + qint64 now = currentDateTime.toSecsSinceEpoch(); + qint64 request_time = now; + qint64 response_time = now; // Algorithm from RFC 2616 section 13.2.3 - int apparent_age = qMax(0, response_time - date_value); - int corrected_received_age = qMax(apparent_age, age_value); - int response_delay = response_time - request_time; - int corrected_initial_age = corrected_received_age + response_delay; - int resident_time = now - response_time; - int current_age = corrected_initial_age + resident_time; + qint64 apparent_age = qMax<qint64>(0, response_time - date_value); + qint64 corrected_received_age = qMax(apparent_age, age_value); + qint64 response_delay = response_time - request_time; + qint64 corrected_initial_age = corrected_received_age + response_delay; + qint64 resident_time = now - response_time; + qint64 current_age = corrected_initial_age + resident_time; - int freshness_lifetime = 0; + qint64 freshness_lifetime = 0; // RFC 2616 13.2.4 Expiration Calculations if (lastModified.isValid() && dateHeader.isValid()) { - int diff = lastModified.secsTo(dateHeader); + qint64 diff = lastModified.secsTo(dateHeader); freshness_lifetime = diff / 10; if (httpRequest.headerField("Warning").isEmpty()) { QDateTime dt = currentDateTime.addSecs(current_age); @@ -601,17 +601,10 @@ void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpReq thread->setObjectName(QStringLiteral("Qt HTTP synchronous thread")); QObject::connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); thread->start(); - } else if (!managerPrivate->httpThread) { + } else { // We use the manager-global thread. // At some point we could switch to having multiple threads if it makes sense. - managerPrivate->httpThread = new QThread(); - managerPrivate->httpThread->setObjectName(QStringLiteral("Qt HTTP thread")); - managerPrivate->httpThread->start(); - - thread = managerPrivate->httpThread; - } else { - // Asynchronous request, thread already exists - thread = managerPrivate->httpThread; + thread = managerPrivate->createThread(); } QUrl url = newHttpRequest.url(); @@ -745,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<QNetworkRequest::LoadControl> (newHttpRequest.attribute(QNetworkRequest::AuthenticationReuseAttribute, QNetworkRequest::Automatic).toInt()) == QNetworkRequest::Manual) @@ -1123,8 +1119,8 @@ void QNetworkReplyHttpImplPrivate::onRedirected(const QUrl &redirectUrl, int htt cookedHeaders.clear(); - if (managerPrivate->httpThread) - managerPrivate->httpThread->disconnect(); + if (managerPrivate->thread) + managerPrivate->thread->disconnect(); // Recurse QMetaObject::invokeMethod(q, "start", Qt::QueuedConnection, diff --git a/src/network/access/qnetworkreplyhttpimpl_p.h b/src/network/access/qnetworkreplyhttpimpl_p.h index 4aba915c7d..868fa617b6 100644 --- a/src/network/access/qnetworkreplyhttpimpl_p.h +++ b/src/network/access/qnetworkreplyhttpimpl_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "qnetworkrequest.h" #include "qnetworkreply.h" diff --git a/src/network/access/qnetworkreplyimpl_p.h b/src/network/access/qnetworkreplyimpl_p.h index 054cbcc3a7..7cd99392d3 100644 --- a/src/network/access/qnetworkreplyimpl_p.h +++ b/src/network/access/qnetworkreplyimpl_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "qnetworkreply.h" #include "qnetworkreply_p.h" #include "qnetworkaccessmanager.h" diff --git a/src/network/access/qnetworkrequest.h b/src/network/access/qnetworkrequest.h index 4a90a50e53..00375f285c 100644 --- a/src/network/access/qnetworkrequest.h +++ b/src/network/access/qnetworkrequest.h @@ -40,6 +40,7 @@ #ifndef QNETWORKREQUEST_H #define QNETWORKREQUEST_H +#include <QtNetwork/qtnetworkglobal.h> #include <QtCore/QSharedDataPointer> #include <QtCore/QString> #include <QtCore/QUrl> @@ -86,6 +87,8 @@ public: BackgroundRequestAttribute, SpdyAllowedAttribute, SpdyWasUsedAttribute, + HTTP2AllowedAttribute, + HTTP2WasUsedAttribute, EmitAllUploadProgressSignalsAttribute, FollowRedirectsAttribute, diff --git a/src/network/access/qnetworkrequest_p.h b/src/network/access/qnetworkrequest_p.h index de6941c476..5e18da6d55 100644 --- a/src/network/access/qnetworkrequest_p.h +++ b/src/network/access/qnetworkrequest_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include "qnetworkrequest.h" #include "QtCore/qbytearray.h" #include "QtCore/qlist.h" diff --git a/src/network/access/qspdyprotocolhandler.cpp b/src/network/access/qspdyprotocolhandler.cpp index 413b03cc22..445a2a1c29 100644 --- a/src/network/access/qspdyprotocolhandler.cpp +++ b/src/network/access/qspdyprotocolhandler.cpp @@ -458,7 +458,7 @@ bool QSpdyProtocolHandler::uncompressHeader(const QByteArray &input, QByteArray break; } default: { - qWarning() << "got unexpected zlib return value:" << zlibRet; + qWarning("got unexpected zlib return value: %d", zlibRet); return false; } } @@ -849,7 +849,7 @@ void QSpdyProtocolHandler::handleControlFrame(const QByteArray &frameHeaders) // break; } default: - qWarning() << "cannot handle frame of type" << type; + qWarning("cannot handle frame of type %d", int(type)); } } @@ -1073,7 +1073,7 @@ void QSpdyProtocolHandler::handleSETTINGS(char flags, quint32 /*length*/, const break; } default: - qWarning() << "found unknown settings value" << value; + qWarning("found unknown settings value %u", uint(value)); } } } @@ -1112,7 +1112,7 @@ void QSpdyProtocolHandler::handleGOAWAY(char /*flags*/, quint32 /*length*/, break; } default: - qWarning() << "unexpected status code" << statusCode; + qWarning("unexpected status code %d", int(statusCode)); errorCode = QNetworkReply::ProtocolUnknownError; } diff --git a/src/network/access/qspdyprotocolhandler_p.h b/src/network/access/qspdyprotocolhandler_p.h index aba081b9d1..0a18505b23 100644 --- a/src/network/access/qspdyprotocolhandler_p.h +++ b/src/network/access/qspdyprotocolhandler_p.h @@ -51,6 +51,7 @@ // We mean it. // +#include <QtNetwork/private/qtnetworkglobal_p.h> #include <private/qabstractprotocolhandler_p.h> #include <QtNetwork/qnetworkreply.h> #include <private/qbytedata_p.h> |