Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
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// Qt-Security score:critical reason:network-protocol
4
5#include "bitstreams_p.h"
6#include "hpack_p.h"
7
8#include <QtCore/qbytearray.h>
9#include <QtCore/qdebug.h>
10
11#include <limits>
12
14
15namespace HPack
16{
17
18HeaderSize header_size(const HttpHeader &header)
19{
20 HeaderSize size(true, 0);
21 for (const HeaderField &field : header) {
22 HeaderSize delta = entry_size(field);
23 if (!delta.first)
24 return HeaderSize();
25 if (std::numeric_limits<quint32>::max() - size.second < delta.second)
26 return HeaderSize();
27 size.second += delta.second;
28 }
29
30 return size;
31}
32
38
40{
41 return lhs.bitLength == rhs.bitLength && lhs.value == rhs.value;
42}
43
44namespace
45{
46
47using StreamError = BitIStream::Error;
48
49// There are several bit patterns to distinguish header fields:
50// 1 - indexed
51// 01 - literal with incremented indexing
52// 0000 - literal without indexing
53// 0001 - literal, never indexing
54// 001 - dynamic table size update.
55
56// It's always 1 or 0 actually, but the number of bits to extract
57// from the input stream - differs.
58constexpr BitPattern Indexed = {1, 1};
59constexpr BitPattern LiteralIncrementalIndexing = {1, 2};
60constexpr BitPattern LiteralNoIndexing = {0, 4};
61constexpr BitPattern LiteralNeverIndexing = {1, 4};
62constexpr BitPattern SizeUpdate = {1, 3};
63
64bool is_literal_field(BitPattern pattern)
65{
66 return pattern == LiteralIncrementalIndexing
67 || pattern == LiteralNoIndexing
68 || pattern == LiteralNeverIndexing;
69}
70
71void write_bit_pattern(BitPattern pattern, BitOStream &outputStream)
72{
73 outputStream.writeBits(pattern.value, pattern.bitLength);
74}
75
76bool read_bit_pattern(BitPattern pattern, BitIStream &inputStream)
77{
78 uchar chunk = 0;
79
80 const quint32 bitsRead = inputStream.peekBits(inputStream.streamOffset(),
81 pattern.bitLength, &chunk);
82 if (bitsRead != pattern.bitLength)
83 return false;
84
85 // Since peekBits packs in the most significant bits, shift it!
86 chunk >>= (8 - bitsRead);
87 if (chunk != pattern.value)
88 return false;
89
90 inputStream.skipBits(pattern.bitLength);
91
92 return true;
93}
94
95bool is_request_pseudo_header(QByteArrayView name)
96{
97 return name == ":method" || name == ":scheme" ||
98 name == ":authority" || name == ":path";
99}
100
101} // unnamed namespace
102
104 : lookupTable(size, true /*encoder needs search index*/),
106{
107}
108
113
118
123
125{
126 if (!header.size()) {
127 qDebug("empty header");
128 return false;
129 }
130
132 return false;
133
134 for (const auto &field : header) {
136 continue;
137
139 return false;
140 }
141
142 return true;
143}
144
146{
147 if (!header.size()) {
148 qDebug("empty header");
149 return false;
150 }
151
153 return false;
154
155 for (const auto &field : header) {
156 if (field.name == ":status")
157 continue;
158
160 return false;
161 }
162
163 return true;
164}
165
167{
169 qDebug("failed to update own table size");
170 return false;
171 }
172
175
176 return true;
177}
178
180{
181 // Up to a caller (HTTP2 protocol handler)
182 // to validate this size first.
184}
185
190
192 const HttpHeader &header)
193{
194 // The following pseudo-header fields are defined for HTTP/2 requests:
195 // - The :method pseudo-header field includes the HTTP method
196 // - The :scheme pseudo-header field includes the scheme portion of the target URI
197 // - The :authority pseudo-header field includes the authority portion of the target URI
198 // - The :path pseudo-header field includes the path and query parts of the target URI
199
200 // All HTTP/2 requests MUST include exactly one valid value for the :method,
201 // :scheme, and :path pseudo-header fields, unless it is a CONNECT request
202 // (Section 8.3). An HTTP request that omits mandatory pseudo-header fields
203 // is malformed (Section 8.1.2.6).
204
205 using size_type = decltype(header.size());
206
207 bool methodFound = false;
208 constexpr QByteArrayView headerName[] = {":authority", ":scheme", ":path"};
209 constexpr size_type nHeaders = std::size(headerName);
210 bool headerFound[nHeaders] = {};
211
212 for (const auto &field : header) {
213 if (field.name == ":status") {
214 qCritical("invalid pseudo-header (:status) in a request");
215 return false;
216 }
217
218 if (field.name == ":method") {
219 if (methodFound) {
220 qCritical("only one :method pseudo-header is allowed");
221 return false;
222 }
223
225 return false;
226 methodFound = true;
227 } else if (field.name == "cookie") {
228 // "crumbs" ...
229 } else {
230 for (size_type j = 0; j < nHeaders; ++j) {
231 if (field.name == headerName[j]) {
232 if (headerFound[j]) {
233 qCritical() << "only one" << headerName[j] << "pseudo-header is allowed";
234 return false;
235 }
237 return false;
238 headerFound[j] = true;
239 break;
240 }
241 }
242 }
243 }
244
245 if (!methodFound) {
246 qCritical("mandatory :method pseudo-header not found");
247 return false;
248 }
249
250 // 1: don't demand headerFound[0], as :authority isn't mandatory.
251 for (size_type i = 1; i < nHeaders; ++i) {
252 if (!headerFound[i]) {
253 qCritical() << "mandatory" << headerName[i]
254 << "pseudo-header not found";
255 return false;
256 }
257 }
258
259 return true;
260}
261
263{
264 // TODO: at the moment we never use LiteralNo/Never Indexing ...
265
266 // Here we try:
267 // 1. indexed
268 // 2. literal indexed with indexed name/literal value
269 // 3. literal indexed with literal name/literal value
270 if (const auto index = lookupTable.indexOf(field.name, field.value))
272
273 if (const auto index = lookupTable.indexOf(field.name)) {
276 }
277
280}
281
283{
284 Q_ASSERT(field.name == ":method");
286 if (index)
288
290 Q_ASSERT(index); // ":method" is always in the static table ...
293}
294
296{
297 bool statusFound = false;
298 for (const auto &field : header) {
300 qCritical() << "invalid pseudo-header" << field.name << "in http response";
301 return false;
302 }
303
304 if (field.name == ":status") {
305 if (statusFound) {
306 qDebug("only one :status pseudo-header is allowed");
307 return false;
308 }
310 return false;
311 statusFound = true;
312 } else if (field.name == "cookie") {
313 // "crumbs"..
314 }
315 }
316
317 if (!statusFound)
318 qCritical("mandatory :status pseudo-header not found");
319
320 return statusFound;
321}
322
324{
326
329
330 return true;
331}
332
334 const QByteArray &name, const QByteArray &value,
335 bool withCompression)
336{
338 // According to HPACK, the bit pattern is
339 // 01 | 000000 (integer 0 that fits into 6-bit prefix),
340 // since integers always end on byte boundary,
341 // this also implies that we always start at bit offset == 0.
342 if (outputStream.bitLength() % 8) {
343 qCritical("invalid bit offset");
344 return false;
345 }
346
349 qDebug("failed to prepend a new field");
350 }
351
353
357
358 return true;
359}
360
363 bool withCompression)
364{
366
368 const bool found = lookupTable.fieldName(nameIndex, &name);
371
374 qDebug("failed to prepend a new field");
375 }
376
380
381 return true;
382}
383
385 : lookupTable{size, false /* we do not need search index ... */}
386{
387}
388
390{
391 header.clear();
392 headerListSize = 0;
393 while (true) {
396 return false;
399 return false;
402 return false;
405 return false;
408 return false;
409 } else {
411 }
412 }
413
414 return false;
415}
416
421
426
431
433{
434 // Up to a caller (HTTP2 protocol handler)
435 // to validate this size first.
437}
438
443
445{
446 quint32 index = 0;
447 if (inputStream.read(&index)) {
448 if (!index) {
449 // "The index value of 0 is not used.
450 // It MUST be treated as a decoding
451 // error if found in an indexed header
452 // field representation."
453 return false;
454 }
455
459 } else {
461 }
462
463 return false;
464}
465
467{
468 // For now, just read and skip bits.
469 quint32 maxSize = 0;
470 if (inputStream.read(&maxSize)) {
472 return false;
473
474 return true;
475 }
476
478 return false;
479}
480
482{
483 // https://http2.github.io/http2-spec/compression.html
484 // 6.2.1, 6.2.2, 6.2.3
485 // Format for all 'literal' is similar,
486 // the difference - is how we update/not our lookup table.
487 quint32 index = 0;
488 if (inputStream.read(&index)) {
490 if (!index) {
491 // Read a string.
492 if (!inputStream.read(&name)) {
494 return false;
495 }
496 } else {
498 return false;
499 }
500
502 if (inputStream.read(&value))
504 }
505
507
508 return false;
509}
510
512 const QByteArray &name,
513 const QByteArray &value)
514{
517 return false;
518 }
519
521 qDebug("about to add a new field, but expected a Dynamic Table Size Update");
522 return false; // We expected a dynamic table size update.
523 }
524
525 const auto [sizeOk, fieldSize] = entry_size(name, value);
528 return false;
529
531 return true;
532}
533
535{
536 const auto errorCode(inputStream.error());
538 return;
539
540 // For now error handling not needed here,
541 // HTTP2 layer will end with session error/COMPRESSION_ERROR.
542}
543
544std::optional<QUrl> makePromiseKeyUrl(const HttpHeader &requestHeader)
545{
546 constexpr QByteArrayView names[] = { ":authority", ":method", ":path", ":scheme" };
547 enum PseudoHeaderEnum
548 {
549 Authority,
550 Method,
551 Path,
552 Scheme
553 };
554 std::array<std::optional<QByteArrayView>, std::size(names)> pseudoHeaders{};
555 for (const auto &field : requestHeader) {
556 const auto *it = std::find(std::begin(names), std::end(names), QByteArrayView(field.name));
557 if (it != std::end(names)) {
558 const auto index = std::distance(std::begin(names), it);
559 if (field.value.isEmpty() || pseudoHeaders.at(index).has_value())
560 return {};
561 pseudoHeaders[index] = field.value;
562 }
563 }
564
565 auto optionalIsSet = [](const auto &x) { return x.has_value(); };
566 if (!std::all_of(pseudoHeaders.begin(), pseudoHeaders.end(), optionalIsSet)) {
567 // All four required, HTTP/2 8.1.2.3.
568 return {};
569 }
570
571 const QByteArrayView method = pseudoHeaders[Method].value();
572 if (method.compare("get", Qt::CaseInsensitive) != 0 &&
573 method.compare("head", Qt::CaseInsensitive) != 0) {
574 return {};
575 }
576
577 QUrl url;
578 url.setScheme(QLatin1StringView(pseudoHeaders[Scheme].value()));
579 url.setAuthority(QLatin1StringView(pseudoHeaders[Authority].value()));
580 url.setPath(QLatin1StringView(pseudoHeaders[Path].value()));
581
582 if (!url.isValid())
583 return {};
584 return url;
585}
586
587}
588
589QT_END_NAMESPACE
HeaderSize header_size(const HttpHeader &header)
Definition hpack.cpp:18
bool operator==(BitPattern lhs, BitPattern rhs)
Definition hpack.cpp:39
std::optional< QUrl > makePromiseKeyUrl(const HttpHeader &requestHeader)
Definition hpack.cpp:544
Combined button and popup list for selecting options.