Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
hpack.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "bitstreams_p.h"
5#include "hpack_p.h"
6
7#include <QtCore/qbytearray.h>
8#include <QtCore/qdebug.h>
9
10#include <limits>
11
13
14namespace HPack
15{
16
18{
19 HeaderSize size(true, 0);
20 for (const HeaderField &field : header) {
21 HeaderSize delta = entry_size(field);
22 if (!delta.first)
23 return HeaderSize();
24 if (std::numeric_limits<quint32>::max() - size.second < delta.second)
25 return HeaderSize();
26 size.second += delta.second;
27 }
28
29 return size;
30}
31
37
38bool operator == (const BitPattern &lhs, const BitPattern &rhs)
39{
40 return lhs.bitLength == rhs.bitLength && lhs.value == rhs.value;
41}
42
43namespace
44{
45
46using StreamError = BitIStream::Error;
47
48// There are several bit patterns to distinguish header fields:
49// 1 - indexed
50// 01 - literal with incremented indexing
51// 0000 - literal without indexing
52// 0001 - literal, never indexing
53// 001 - dynamic table size update.
54
55// It's always 1 or 0 actually, but the number of bits to extract
56// from the input stream - differs.
57const BitPattern Indexed = {1, 1};
58const BitPattern LiteralIncrementalIndexing = {1, 2};
59const BitPattern LiteralNoIndexing = {0, 4};
60const BitPattern LiteralNeverIndexing = {1, 4};
61const BitPattern SizeUpdate = {1, 3};
62
63bool is_literal_field(const BitPattern &pattern)
64{
65 return pattern == LiteralIncrementalIndexing
66 || pattern == LiteralNoIndexing
67 || pattern == LiteralNeverIndexing;
68}
69
70void write_bit_pattern(const BitPattern &pattern, BitOStream &outputStream)
71{
72 outputStream.writeBits(pattern.value, pattern.bitLength);
73}
74
75bool read_bit_pattern(const BitPattern &pattern, BitIStream &inputStream)
76{
77 uchar chunk = 0;
78
79 const quint32 bitsRead = inputStream.peekBits(inputStream.streamOffset(),
80 pattern.bitLength, &chunk);
81 if (bitsRead != pattern.bitLength)
82 return false;
83
84 // Since peekBits packs in the most significant bits, shift it!
85 chunk >>= (8 - bitsRead);
86 if (chunk != pattern.value)
87 return false;
88
89 inputStream.skipBits(pattern.bitLength);
90
91 return true;
92}
93
94bool is_request_pseudo_header(QByteArrayView name)
95{
96 return name == ":method" || name == ":scheme" ||
97 name == ":authority" || name == ":path";
98}
99
100} // unnamed namespace
101
103 : lookupTable(size, true /*encoder needs search index*/),
104 compressStrings(compress)
105{
106}
107
109{
110 return lookupTable.dynamicDataSize();
111}
112
114{
115 if (!header.size()) {
116 qDebug("empty header");
117 return false;
118 }
119
120 if (!encodeRequestPseudoHeaders(outputStream, header))
121 return false;
122
123 for (const auto &field : header) {
124 if (is_request_pseudo_header(field.name))
125 continue;
126
127 if (!encodeHeaderField(outputStream, field))
128 return false;
129 }
130
131 return true;
132}
133
135{
136 if (!header.size()) {
137 qDebug("empty header");
138 return false;
139 }
140
141 if (!encodeResponsePseudoHeaders(outputStream, header))
142 return false;
143
144 for (const auto &field : header) {
145 if (field.name == ":status")
146 continue;
147
148 if (!encodeHeaderField(outputStream, field))
149 return false;
150 }
151
152 return true;
153}
154
155bool Encoder::encodeSizeUpdate(BitOStream &outputStream, quint32 newSize)
156{
157 if (!lookupTable.updateDynamicTableSize(newSize)) {
158 qDebug("failed to update own table size");
159 return false;
160 }
161
162 write_bit_pattern(SizeUpdate, outputStream);
163 outputStream.write(newSize);
164
165 return true;
166}
167
169{
170 // Up to a caller (HTTP2 protocol handler)
171 // to validate this size first.
172 lookupTable.setMaxDynamicTableSize(size);
173}
174
176{
177 compressStrings = compress;
178}
179
180bool Encoder::encodeRequestPseudoHeaders(BitOStream &outputStream,
181 const HttpHeader &header)
182{
183 // The following pseudo-header fields are defined for HTTP/2 requests:
184 // - The :method pseudo-header field includes the HTTP method
185 // - The :scheme pseudo-header field includes the scheme portion of the target URI
186 // - The :authority pseudo-header field includes the authority portion of the target URI
187 // - The :path pseudo-header field includes the path and query parts of the target URI
188
189 // All HTTP/2 requests MUST include exactly one valid value for the :method,
190 // :scheme, and :path pseudo-header fields, unless it is a CONNECT request
191 // (Section 8.3). An HTTP request that omits mandatory pseudo-header fields
192 // is malformed (Section 8.1.2.6).
193
194 using size_type = decltype(header.size());
195
196 bool methodFound = false;
197 constexpr QByteArrayView headerName[] = {":authority", ":scheme", ":path"};
198 constexpr size_type nHeaders = std::size(headerName);
199 bool headerFound[nHeaders] = {};
200
201 for (const auto &field : header) {
202 if (field.name == ":status") {
203 qCritical("invalid pseudo-header (:status) in a request");
204 return false;
205 }
206
207 if (field.name == ":method") {
208 if (methodFound) {
209 qCritical("only one :method pseudo-header is allowed");
210 return false;
211 }
212
213 if (!encodeMethod(outputStream, field))
214 return false;
215 methodFound = true;
216 } else if (field.name == "cookie") {
217 // "crumbs" ...
218 } else {
219 for (size_type j = 0; j < nHeaders; ++j) {
220 if (field.name == headerName[j]) {
221 if (headerFound[j]) {
222 qCritical() << "only one" << headerName[j] << "pseudo-header is allowed";
223 return false;
224 }
225 if (!encodeHeaderField(outputStream, field))
226 return false;
227 headerFound[j] = true;
228 break;
229 }
230 }
231 }
232 }
233
234 if (!methodFound) {
235 qCritical("mandatory :method pseudo-header not found");
236 return false;
237 }
238
239 // 1: don't demand headerFound[0], as :authority isn't mandatory.
240 for (size_type i = 1; i < nHeaders; ++i) {
241 if (!headerFound[i]) {
242 qCritical() << "mandatory" << headerName[i]
243 << "pseudo-header not found";
244 return false;
245 }
246 }
247
248 return true;
249}
250
251bool Encoder::encodeHeaderField(BitOStream &outputStream, const HeaderField &field)
252{
253 // TODO: at the moment we never use LiteralNo/Never Indexing ...
254
255 // Here we try:
256 // 1. indexed
257 // 2. literal indexed with indexed name/literal value
258 // 3. literal indexed with literal name/literal value
259 if (const auto index = lookupTable.indexOf(field.name, field.value))
260 return encodeIndexedField(outputStream, index);
261
262 if (const auto index = lookupTable.indexOf(field.name)) {
263 return encodeLiteralField(outputStream, LiteralIncrementalIndexing,
264 index, field.value, compressStrings);
265 }
266
267 return encodeLiteralField(outputStream, LiteralIncrementalIndexing,
268 field.name, field.value, compressStrings);
269}
270
271bool Encoder::encodeMethod(BitOStream &outputStream, const HeaderField &field)
272{
273 Q_ASSERT(field.name == ":method");
274 quint32 index = lookupTable.indexOf(field.name, field.value);
275 if (index)
276 return encodeIndexedField(outputStream, index);
277
278 index = lookupTable.indexOf(field.name);
279 Q_ASSERT(index); // ":method" is always in the static table ...
280 return encodeLiteralField(outputStream, LiteralIncrementalIndexing,
281 index, field.value, compressStrings);
282}
283
284bool Encoder::encodeResponsePseudoHeaders(BitOStream &outputStream, const HttpHeader &header)
285{
286 bool statusFound = false;
287 for (const auto &field : header) {
288 if (is_request_pseudo_header(field.name)) {
289 qCritical() << "invalid pseudo-header" << field.name << "in http response";
290 return false;
291 }
292
293 if (field.name == ":status") {
294 if (statusFound) {
295 qDebug("only one :status pseudo-header is allowed");
296 return false;
297 }
298 if (!encodeHeaderField(outputStream, field))
299 return false;
300 statusFound = true;
301 } else if (field.name == "cookie") {
302 // "crumbs"..
303 }
304 }
305
306 if (!statusFound)
307 qCritical("mandatory :status pseudo-header not found");
308
309 return statusFound;
310}
311
312bool Encoder::encodeIndexedField(BitOStream &outputStream, quint32 index) const
313{
314 Q_ASSERT(lookupTable.indexIsValid(index));
315
316 write_bit_pattern(Indexed, outputStream);
317 outputStream.write(index);
318
319 return true;
320}
321
322bool Encoder::encodeLiteralField(BitOStream &outputStream, const BitPattern &fieldType,
323 const QByteArray &name, const QByteArray &value,
324 bool withCompression)
325{
326 Q_ASSERT(is_literal_field(fieldType));
327 // According to HPACK, the bit pattern is
328 // 01 | 000000 (integer 0 that fits into 6-bit prefix),
329 // since integers always end on byte boundary,
330 // this also implies that we always start at bit offset == 0.
331 if (outputStream.bitLength() % 8) {
332 qCritical("invalid bit offset");
333 return false;
334 }
335
336 if (fieldType == LiteralIncrementalIndexing) {
337 if (!lookupTable.prependField(name, value))
338 qDebug("failed to prepend a new field");
339 }
340
341 write_bit_pattern(fieldType, outputStream);
342
343 outputStream.write(0);
344 outputStream.write(name, withCompression);
345 outputStream.write(value, withCompression);
346
347 return true;
348}
349
350bool Encoder::encodeLiteralField(BitOStream &outputStream, const BitPattern &fieldType,
351 quint32 nameIndex, const QByteArray &value,
352 bool withCompression)
353{
354 Q_ASSERT(is_literal_field(fieldType));
355
357 const bool found = lookupTable.fieldName(nameIndex, &name);
358 Q_UNUSED(found);
359 Q_ASSERT(found);
360
361 if (fieldType == LiteralIncrementalIndexing) {
362 if (!lookupTable.prependField(name, value))
363 qDebug("failed to prepend a new field");
364 }
365
366 write_bit_pattern(fieldType, outputStream);
367 outputStream.write(nameIndex);
368 outputStream.write(value, withCompression);
369
370 return true;
371}
372
374 : lookupTable{size, false /* we do not need search index ... */}
375{
376}
377
379{
380 header.clear();
381 while (true) {
382 if (read_bit_pattern(Indexed, inputStream)) {
383 if (!decodeIndexedField(inputStream))
384 return false;
385 } else if (read_bit_pattern(LiteralIncrementalIndexing, inputStream)) {
386 if (!decodeLiteralField(LiteralIncrementalIndexing, inputStream))
387 return false;
388 } else if (read_bit_pattern(LiteralNoIndexing, inputStream)) {
389 if (!decodeLiteralField(LiteralNoIndexing, inputStream))
390 return false;
391 } else if (read_bit_pattern(LiteralNeverIndexing, inputStream)) {
392 if (!decodeLiteralField(LiteralNeverIndexing, inputStream))
393 return false;
394 } else if (read_bit_pattern(SizeUpdate, inputStream)) {
395 if (!decodeSizeUpdate(inputStream))
396 return false;
397 } else {
398 return inputStream.bitLength() == inputStream.streamOffset();
399 }
400 }
401
402 return false;
403}
404
406{
407 return lookupTable.dynamicDataSize();
408}
409
411{
412 // Up to a caller (HTTP2 protocol handler)
413 // to validate this size first.
414 lookupTable.setMaxDynamicTableSize(size);
415}
416
417bool Decoder::decodeIndexedField(BitIStream &inputStream)
418{
419 quint32 index = 0;
420 if (inputStream.read(&index)) {
421 if (!index) {
422 // "The index value of 0 is not used.
423 // It MUST be treated as a decoding
424 // error if found in an indexed header
425 // field representation."
426 return false;
427 }
428
430 if (lookupTable.field(index, &name, &value))
431 return processDecodedField(Indexed, name, value);
432 } else {
433 handleStreamError(inputStream);
434 }
435
436 return false;
437}
438
439bool Decoder::decodeSizeUpdate(BitIStream &inputStream)
440{
441 // For now, just read and skip bits.
442 quint32 maxSize = 0;
443 if (inputStream.read(&maxSize)) {
444 if (!lookupTable.updateDynamicTableSize(maxSize))
445 return false;
446
447 return true;
448 }
449
450 handleStreamError(inputStream);
451 return false;
452}
453
454bool Decoder::decodeLiteralField(const BitPattern &fieldType, BitIStream &inputStream)
455{
456 // https://http2.github.io/http2-spec/compression.html
457 // 6.2.1, 6.2.2, 6.2.3
458 // Format for all 'literal' is similar,
459 // the difference - is how we update/not our lookup table.
460 quint32 index = 0;
461 if (inputStream.read(&index)) {
463 if (!index) {
464 // Read a string.
465 if (!inputStream.read(&name)) {
466 handleStreamError(inputStream);
467 return false;
468 }
469 } else {
470 if (!lookupTable.fieldName(index, &name))
471 return false;
472 }
473
475 if (inputStream.read(&value))
476 return processDecodedField(fieldType, name, value);
477 }
478
479 handleStreamError(inputStream);
480
481 return false;
482}
483
484bool Decoder::processDecodedField(const BitPattern &fieldType,
485 const QByteArray &name,
486 const QByteArray &value)
487{
488 if (fieldType == LiteralIncrementalIndexing) {
489 if (!lookupTable.prependField(name, value))
490 return false;
491 }
492
493 header.push_back(HeaderField(name, value));
494 return true;
495}
496
497void Decoder::handleStreamError(BitIStream &inputStream)
498{
499 const auto errorCode(inputStream.error());
500 if (errorCode == StreamError::NoError)
501 return;
502
503 // For now error handling not needed here,
504 // HTTP2 layer will end with session error/COMPRESSION_ERROR.
505}
506
507std::optional<QUrl> makePromiseKeyUrl(const HttpHeader &requestHeader)
508{
509 constexpr QByteArrayView names[] = { ":authority", ":method", ":path", ":scheme" };
510 enum PseudoHeaderEnum
511 {
512 Authority,
513 Method,
514 Path,
515 Scheme
516 };
517 std::array<std::optional<QByteArrayView>, std::size(names)> pseudoHeaders{};
518 for (const auto &field : requestHeader) {
519 const auto *it = std::find(std::begin(names), std::end(names), QByteArrayView(field.name));
520 if (it != std::end(names)) {
521 const auto index = std::distance(std::begin(names), it);
522 if (field.value.isEmpty() || pseudoHeaders.at(index).has_value())
523 return {};
524 pseudoHeaders[index] = field.value;
525 }
526 }
527
528 auto optionalIsSet = [](const auto &x) { return x.has_value(); };
529 if (!std::all_of(pseudoHeaders.begin(), pseudoHeaders.end(), optionalIsSet)) {
530 // All four required, HTTP/2 8.1.2.3.
531 return {};
532 }
533
534 const QByteArrayView method = pseudoHeaders[Method].value();
535 if (method.compare("get", Qt::CaseInsensitive) != 0 &&
536 method.compare("head", Qt::CaseInsensitive) != 0) {
537 return {};
538 }
539
540 QUrl url;
541 url.setScheme(QLatin1StringView(pseudoHeaders[Scheme].value()));
542 url.setAuthority(QLatin1StringView(pseudoHeaders[Authority].value()));
543 url.setPath(QLatin1StringView(pseudoHeaders[Path].value()));
544
545 if (!url.isValid())
546 return {};
547 return url;
548}
549
550}
551
quint64 bitLength() const
quint64 streamOffset() const
bool read(quint32 *dstPtr)
void write(quint32 src)
quint32 dynamicTableSize() const
Definition hpack.cpp:405
void setMaxDynamicTableSize(quint32 size)
Definition hpack.cpp:410
bool decodeHeaderFields(class BitIStream &inputStream)
Definition hpack.cpp:378
quint32 dynamicTableSize() const
Definition hpack.cpp:108
void setCompressStrings(bool compress)
Definition hpack.cpp:175
void setMaxDynamicTableSize(quint32 size)
Definition hpack.cpp:168
bool encodeResponse(BitOStream &outputStream, const HttpHeader &header)
Definition hpack.cpp:134
bool encodeRequest(class BitOStream &outputStream, const HttpHeader &header)
Definition hpack.cpp:113
bool encodeSizeUpdate(BitOStream &outputStream, quint32 newSize)
Definition hpack.cpp:155
Encoder(quint32 maxTableSize, bool compressStrings)
Definition hpack.cpp:102
bool fieldName(quint32 index, QByteArray *dst) const
void setMaxDynamicTableSize(quint32 size)
bool updateDynamicTableSize(quint32 size)
quint32 indexOf(const QByteArray &name, const QByteArray &value) const
bool indexIsValid(quint32 index) const
bool field(quint32 index, QByteArray *name, QByteArray *value) const
quint32 dynamicDataSize() const
bool prependField(const QByteArray &name, const QByteArray &value)
\inmodule QtCore
Definition qbytearray.h:57
\inmodule QtCore
Definition qurl.h:94
bool isValid() const
Returns true if the URL is non-empty and valid; otherwise returns false.
Definition qurl.cpp:1882
void setScheme(const QString &scheme)
Sets the scheme of the URL to scheme.
Definition qurl.cpp:1967
void setAuthority(const QString &authority, ParsingMode mode=TolerantMode)
Sets the authority of the URL to authority.
Definition qurl.cpp:2027
void setPath(const QString &path, ParsingMode mode=DecodedMode)
Sets the path of the URL to path.
Definition qurl.cpp:2414
QSet< QString >::iterator it
bool operator==(const BitPattern &lhs, const BitPattern &rhs)
Definition hpack.cpp:38
QPair< bool, quint32 > HeaderSize
std::vector< HeaderField > HttpHeader
Definition hpack_p.h:33
HeaderSize header_size(const HttpHeader &header)
Definition hpack.cpp:17
std::optional< QUrl > makePromiseKeyUrl(const HttpHeader &requestHeader)
Definition hpack.cpp:507
HeaderSize entry_size(QByteArrayView name, QByteArrayView value)
Combined button and popup list for selecting options.
@ CaseInsensitive
DBusConnection const char DBusError DBusBusType DBusError return DBusConnection DBusHandleMessageFunction void DBusFreeFunction return DBusConnection return DBusConnection return const char DBusError return DBusConnection DBusMessage dbus_uint32_t return DBusConnection dbus_bool_t DBusConnection DBusAddWatchFunction DBusRemoveWatchFunction DBusWatchToggledFunction void DBusFreeFunction return DBusConnection DBusDispatchStatusFunction void DBusFreeFunction DBusTimeout return DBusTimeout return DBusWatch return DBusWatch unsigned int return DBusError const DBusError return const DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessageIter int const void return DBusMessageIter DBusMessageIter return DBusMessageIter void DBusMessageIter void int return DBusMessage DBusMessageIter return DBusMessageIter return DBusMessageIter DBusMessageIter const char const char const char const char * method
static QString header(const QString &name)
EGLOutputLayerEXT EGLint EGLAttrib value
[5]
#define qCritical
Definition qlogging.h:167
#define qDebug
[1]
Definition qlogging.h:164
GLint GLint GLint GLint GLint x
[0]
GLenum GLuint GLintptr GLsizeiptr size
[1]
GLuint index
[2]
GLuint name
GLuint GLuint * names
GLubyte * pattern
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
#define Q_UNUSED(x)
unsigned int quint32
Definition qtypes.h:50
unsigned char uchar
Definition qtypes.h:32
QUrl url("example.com")
[constructor-url-reference]