| /**************************************************************************** |
| ** |
| ** 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 |
| { |
| 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); |
| } |
| |
| void Encoder::setCompressStrings(bool compress) |
| { |
| compressStrings = compress; |
| } |
| |
| 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 |