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
13QT_BEGIN_NAMESPACE
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(entry: 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
33struct BitPattern
34{
35 uchar value;
36 uchar bitLength;
37};
38
39bool operator==(BitPattern lhs, BitPattern rhs)
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 = {.value: 1, .bitLength: 1};
59constexpr BitPattern LiteralIncrementalIndexing = {.value: 1, .bitLength: 2};
60constexpr BitPattern LiteralNoIndexing = {.value: 0, .bitLength: 4};
61constexpr BitPattern LiteralNeverIndexing = {.value: 1, .bitLength: 4};
62constexpr BitPattern SizeUpdate = {.value: 1, .bitLength: 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(bits: pattern.value, bitLength: pattern.bitLength);
74}
75
76bool read_bit_pattern(BitPattern pattern, BitIStream &inputStream)
77{
78 uchar chunk = 0;
79
80 const quint32 bitsRead = inputStream.peekBits(from: inputStream.streamOffset(),
81 length: pattern.bitLength, dstPtr: &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(nBits: 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
103Encoder::Encoder(quint32 size, bool compress)
104 : lookupTable(size, true /*encoder needs search index*/),
105 compressStrings(compress)
106{
107}
108
109quint32 Encoder::dynamicTableSize() const
110{
111 return lookupTable.dynamicDataSize();
112}
113
114quint32 Encoder::dynamicTableCapacity() const
115{
116 return lookupTable.dynamicDataCapacity();
117}
118
119quint32 Encoder::maxDynamicTableCapacity() const
120{
121 return lookupTable.maxDynamicDataCapacity();
122}
123
124bool Encoder::encodeRequest(BitOStream &outputStream, const HttpHeader &header)
125{
126 if (!header.size()) {
127 qDebug(msg: "empty header");
128 return false;
129 }
130
131 if (!encodeRequestPseudoHeaders(outputStream, header))
132 return false;
133
134 for (const auto &field : header) {
135 if (is_request_pseudo_header(name: field.name))
136 continue;
137
138 if (!encodeHeaderField(outputStream, field))
139 return false;
140 }
141
142 return true;
143}
144
145bool Encoder::encodeResponse(BitOStream &outputStream, const HttpHeader &header)
146{
147 if (!header.size()) {
148 qDebug(msg: "empty header");
149 return false;
150 }
151
152 if (!encodeResponsePseudoHeaders(outputStream, header))
153 return false;
154
155 for (const auto &field : header) {
156 if (field.name == ":status")
157 continue;
158
159 if (!encodeHeaderField(outputStream, field))
160 return false;
161 }
162
163 return true;
164}
165
166bool Encoder::encodeSizeUpdate(BitOStream &outputStream, quint32 newSize)
167{
168 if (!lookupTable.updateDynamicTableSize(size: newSize)) {
169 qDebug(msg: "failed to update own table size");
170 return false;
171 }
172
173 write_bit_pattern(pattern: SizeUpdate, outputStream);
174 outputStream.write(src: newSize);
175
176 return true;
177}
178
179void Encoder::setMaxDynamicTableSize(quint32 size)
180{
181 // Up to a caller (HTTP2 protocol handler)
182 // to validate this size first.
183 lookupTable.setMaxDynamicTableSize(size);
184}
185
186void Encoder::setCompressStrings(bool compress)
187{
188 compressStrings = compress;
189}
190
191bool Encoder::encodeRequestPseudoHeaders(BitOStream &outputStream,
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(msg: "invalid pseudo-header (:status) in a request");
215 return false;
216 }
217
218 if (field.name == ":method") {
219 if (methodFound) {
220 qCritical(msg: "only one :method pseudo-header is allowed");
221 return false;
222 }
223
224 if (!encodeMethod(outputStream, field))
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 }
236 if (!encodeHeaderField(outputStream, field))
237 return false;
238 headerFound[j] = true;
239 break;
240 }
241 }
242 }
243 }
244
245 if (!methodFound) {
246 qCritical(msg: "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
262bool Encoder::encodeHeaderField(BitOStream &outputStream, const HeaderField &field)
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(name: field.name, value: field.value))
271 return encodeIndexedField(outputStream, index);
272
273 if (const auto index = lookupTable.indexOf(name: field.name)) {
274 return encodeLiteralField(outputStream, fieldType: LiteralIncrementalIndexing,
275 nameIndex: index, value: field.value, withCompression: compressStrings);
276 }
277
278 return encodeLiteralField(outputStream, fieldType: LiteralIncrementalIndexing,
279 name: field.name, value: field.value, withCompression: compressStrings);
280}
281
282bool Encoder::encodeMethod(BitOStream &outputStream, const HeaderField &field)
283{
284 Q_ASSERT(field.name == ":method");
285 quint32 index = lookupTable.indexOf(name: field.name, value: field.value);
286 if (index)
287 return encodeIndexedField(outputStream, index);
288
289 index = lookupTable.indexOf(name: field.name);
290 Q_ASSERT(index); // ":method" is always in the static table ...
291 return encodeLiteralField(outputStream, fieldType: LiteralIncrementalIndexing,
292 nameIndex: index, value: field.value, withCompression: compressStrings);
293}
294
295bool Encoder::encodeResponsePseudoHeaders(BitOStream &outputStream, const HttpHeader &header)
296{
297 bool statusFound = false;
298 for (const auto &field : header) {
299 if (is_request_pseudo_header(name: field.name)) {
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(msg: "only one :status pseudo-header is allowed");
307 return false;
308 }
309 if (!encodeHeaderField(outputStream, field))
310 return false;
311 statusFound = true;
312 } else if (field.name == "cookie") {
313 // "crumbs"..
314 }
315 }
316
317 if (!statusFound)
318 qCritical(msg: "mandatory :status pseudo-header not found");
319
320 return statusFound;
321}
322
323bool Encoder::encodeIndexedField(BitOStream &outputStream, quint32 index) const
324{
325 Q_ASSERT(lookupTable.indexIsValid(index));
326
327 write_bit_pattern(pattern: Indexed, outputStream);
328 outputStream.write(src: index);
329
330 return true;
331}
332
333bool Encoder::encodeLiteralField(BitOStream &outputStream, BitPattern fieldType,
334 const QByteArray &name, const QByteArray &value,
335 bool withCompression)
336{
337 Q_ASSERT(is_literal_field(fieldType));
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(msg: "invalid bit offset");
344 return false;
345 }
346
347 if (fieldType == LiteralIncrementalIndexing) {
348 if (!lookupTable.prependField(name, value))
349 qDebug(msg: "failed to prepend a new field");
350 }
351
352 write_bit_pattern(pattern: fieldType, outputStream);
353
354 outputStream.write(src: 0);
355 outputStream.write(src: name, compressed: withCompression);
356 outputStream.write(src: value, compressed: withCompression);
357
358 return true;
359}
360
361bool Encoder::encodeLiteralField(BitOStream &outputStream, BitPattern fieldType,
362 quint32 nameIndex, const QByteArray &value,
363 bool withCompression)
364{
365 Q_ASSERT(is_literal_field(fieldType));
366
367 QByteArray name;
368 const bool found = lookupTable.fieldName(index: nameIndex, dst: &name);
369 Q_UNUSED(found);
370 Q_ASSERT(found);
371
372 if (fieldType == LiteralIncrementalIndexing) {
373 if (!lookupTable.prependField(name, value))
374 qDebug(msg: "failed to prepend a new field");
375 }
376
377 write_bit_pattern(pattern: fieldType, outputStream);
378 outputStream.write(src: nameIndex);
379 outputStream.write(src: value, compressed: withCompression);
380
381 return true;
382}
383
384Decoder::Decoder(quint32 size)
385 : lookupTable{size, false /* we do not need search index ... */}
386{
387}
388
389bool Decoder::decodeHeaderFields(BitIStream &inputStream)
390{
391 header.clear();
392 while (true) {
393 if (read_bit_pattern(pattern: Indexed, inputStream)) {
394 if (!decodeIndexedField(inputStream))
395 return false;
396 } else if (read_bit_pattern(pattern: LiteralIncrementalIndexing, inputStream)) {
397 if (!decodeLiteralField(fieldType: LiteralIncrementalIndexing, inputStream))
398 return false;
399 } else if (read_bit_pattern(pattern: LiteralNoIndexing, inputStream)) {
400 if (!decodeLiteralField(fieldType: LiteralNoIndexing, inputStream))
401 return false;
402 } else if (read_bit_pattern(pattern: LiteralNeverIndexing, inputStream)) {
403 if (!decodeLiteralField(fieldType: LiteralNeverIndexing, inputStream))
404 return false;
405 } else if (read_bit_pattern(pattern: SizeUpdate, inputStream)) {
406 if (!decodeSizeUpdate(inputStream))
407 return false;
408 } else {
409 return inputStream.bitLength() == inputStream.streamOffset();
410 }
411 }
412
413 return false;
414}
415
416quint32 Decoder::dynamicTableSize() const
417{
418 return lookupTable.dynamicDataSize();
419}
420
421quint32 Decoder::dynamicTableCapacity() const
422{
423 return lookupTable.dynamicDataCapacity();
424}
425
426quint32 Decoder::maxDynamicTableCapacity() const
427{
428 return lookupTable.maxDynamicDataCapacity();
429}
430
431void Decoder::setMaxDynamicTableSize(quint32 size)
432{
433 // Up to a caller (HTTP2 protocol handler)
434 // to validate this size first.
435 lookupTable.setMaxDynamicTableSize(size);
436}
437
438bool Decoder::decodeIndexedField(BitIStream &inputStream)
439{
440 quint32 index = 0;
441 if (inputStream.read(dstPtr: &index)) {
442 if (!index) {
443 // "The index value of 0 is not used.
444 // It MUST be treated as a decoding
445 // error if found in an indexed header
446 // field representation."
447 return false;
448 }
449
450 QByteArray name, value;
451 if (lookupTable.field(index, name: &name, value: &value))
452 return processDecodedField(fieldType: Indexed, name, value);
453 } else {
454 handleStreamError(inputStream);
455 }
456
457 return false;
458}
459
460bool Decoder::decodeSizeUpdate(BitIStream &inputStream)
461{
462 // For now, just read and skip bits.
463 quint32 maxSize = 0;
464 if (inputStream.read(dstPtr: &maxSize)) {
465 if (!lookupTable.updateDynamicTableSize(size: maxSize))
466 return false;
467
468 return true;
469 }
470
471 handleStreamError(inputStream);
472 return false;
473}
474
475bool Decoder::decodeLiteralField(BitPattern fieldType, BitIStream &inputStream)
476{
477 // https://http2.github.io/http2-spec/compression.html
478 // 6.2.1, 6.2.2, 6.2.3
479 // Format for all 'literal' is similar,
480 // the difference - is how we update/not our lookup table.
481 quint32 index = 0;
482 if (inputStream.read(dstPtr: &index)) {
483 QByteArray name;
484 if (!index) {
485 // Read a string.
486 if (!inputStream.read(dstPtr: &name)) {
487 handleStreamError(inputStream);
488 return false;
489 }
490 } else {
491 if (!lookupTable.fieldName(index, dst: &name))
492 return false;
493 }
494
495 QByteArray value;
496 if (inputStream.read(dstPtr: &value))
497 return processDecodedField(fieldType, name, value);
498 }
499
500 handleStreamError(inputStream);
501
502 return false;
503}
504
505bool Decoder::processDecodedField(BitPattern fieldType,
506 const QByteArray &name,
507 const QByteArray &value)
508{
509 if (fieldType == LiteralIncrementalIndexing) {
510 if (!lookupTable.prependField(name, value))
511 return false;
512 }
513
514 if (lookupTable.maxDynamicDataCapacity() < lookupTable.dynamicDataCapacity()) {
515 qDebug(msg: "about to add a new field, but expected a Dynamic Table Size Update");
516 return false; // We expected a dynamic table size update.
517 }
518
519 header.push_back(x: HeaderField(name, value));
520 return true;
521}
522
523void Decoder::handleStreamError(BitIStream &inputStream)
524{
525 const auto errorCode(inputStream.error());
526 if (errorCode == StreamError::NoError)
527 return;
528
529 // For now error handling not needed here,
530 // HTTP2 layer will end with session error/COMPRESSION_ERROR.
531}
532
533std::optional<QUrl> makePromiseKeyUrl(const HttpHeader &requestHeader)
534{
535 constexpr QByteArrayView names[] = { ":authority", ":method", ":path", ":scheme" };
536 enum PseudoHeaderEnum
537 {
538 Authority,
539 Method,
540 Path,
541 Scheme
542 };
543 std::array<std::optional<QByteArrayView>, std::size(names)> pseudoHeaders{};
544 for (const auto &field : requestHeader) {
545 const auto *it = std::find(first: std::begin(arr: names), last: std::end(arr: names), val: QByteArrayView(field.name));
546 if (it != std::end(arr: names)) {
547 const auto index = std::distance(first: std::begin(arr: names), last: it);
548 if (field.value.isEmpty() || pseudoHeaders.at(n: index).has_value())
549 return {};
550 pseudoHeaders[index] = field.value;
551 }
552 }
553
554 auto optionalIsSet = [](const auto &x) { return x.has_value(); };
555 if (!std::all_of(first: pseudoHeaders.begin(), last: pseudoHeaders.end(), pred: optionalIsSet)) {
556 // All four required, HTTP/2 8.1.2.3.
557 return {};
558 }
559
560 const QByteArrayView method = pseudoHeaders[Method].value();
561 if (method.compare(a: "get", cs: Qt::CaseInsensitive) != 0 &&
562 method.compare(a: "head", cs: Qt::CaseInsensitive) != 0) {
563 return {};
564 }
565
566 QUrl url;
567 url.setScheme(QLatin1StringView(pseudoHeaders[Scheme].value()));
568 url.setAuthority(authority: QLatin1StringView(pseudoHeaders[Authority].value()));
569 url.setPath(path: QLatin1StringView(pseudoHeaders[Path].value()));
570
571 if (!url.isValid())
572 return {};
573 return url;
574}
575
576}
577
578QT_END_NAMESPACE
579

source code of qtbase/src/network/access/http2/hpack.cpp