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

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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