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 | |
12 | QT_BEGIN_NAMESPACE |
13 | |
14 | namespace HPack |
15 | { |
16 | |
17 | HeaderSize 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 | |
32 | struct BitPattern |
33 | { |
34 | uchar value; |
35 | uchar bitLength; |
36 | }; |
37 | |
38 | bool operator==(BitPattern lhs, BitPattern rhs) |
39 | { |
40 | return lhs.bitLength == rhs.bitLength && lhs.value == rhs.value; |
41 | } |
42 | |
43 | namespace |
44 | { |
45 | |
46 | using 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. |
57 | constexpr BitPattern Indexed = {.value: 1, .bitLength: 1}; |
58 | constexpr BitPattern LiteralIncrementalIndexing = {.value: 1, .bitLength: 2}; |
59 | constexpr BitPattern LiteralNoIndexing = {.value: 0, .bitLength: 4}; |
60 | constexpr BitPattern LiteralNeverIndexing = {.value: 1, .bitLength: 4}; |
61 | constexpr BitPattern SizeUpdate = {.value: 1, .bitLength: 3}; |
62 | |
63 | bool is_literal_field(BitPattern pattern) |
64 | { |
65 | return pattern == LiteralIncrementalIndexing |
66 | || pattern == LiteralNoIndexing |
67 | || pattern == LiteralNeverIndexing; |
68 | } |
69 | |
70 | void write_bit_pattern(BitPattern pattern, BitOStream &outputStream) |
71 | { |
72 | outputStream.writeBits(bits: pattern.value, bitLength: pattern.bitLength); |
73 | } |
74 | |
75 | bool 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 | |
94 | bool is_request_pseudo_header(QByteArrayView name) |
95 | { |
96 | return name == ":method"|| name == ":scheme"|| |
97 | name == ":authority"|| name == ":path"; |
98 | } |
99 | |
100 | } // unnamed namespace |
101 | |
102 | Encoder::Encoder(quint32 size, bool compress) |
103 | : lookupTable(size, true /*encoder needs search index*/), |
104 | compressStrings(compress) |
105 | { |
106 | } |
107 | |
108 | quint32 Encoder::dynamicTableSize() const |
109 | { |
110 | return lookupTable.dynamicDataSize(); |
111 | } |
112 | |
113 | quint32 Encoder::dynamicTableCapacity() const |
114 | { |
115 | return lookupTable.dynamicDataCapacity(); |
116 | } |
117 | |
118 | quint32 Encoder::maxDynamicTableCapacity() const |
119 | { |
120 | return lookupTable.maxDynamicDataCapacity(); |
121 | } |
122 | |
123 | bool 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 | |
144 | bool 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 | |
165 | bool 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 | |
178 | void 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 | |
185 | void Encoder::setCompressStrings(bool compress) |
186 | { |
187 | compressStrings = compress; |
188 | } |
189 | |
190 | bool 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 | |
261 | bool 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 | |
281 | bool 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 | |
294 | bool 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 | |
322 | bool 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 | |
332 | bool 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 | |
360 | bool 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 | |
383 | Decoder::Decoder(quint32 size) |
384 | : lookupTable{size, false /* we do not need search index ... */} |
385 | { |
386 | } |
387 | |
388 | bool 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 | |
415 | quint32 Decoder::dynamicTableSize() const |
416 | { |
417 | return lookupTable.dynamicDataSize(); |
418 | } |
419 | |
420 | quint32 Decoder::dynamicTableCapacity() const |
421 | { |
422 | return lookupTable.dynamicDataCapacity(); |
423 | } |
424 | |
425 | quint32 Decoder::maxDynamicTableCapacity() const |
426 | { |
427 | return lookupTable.maxDynamicDataCapacity(); |
428 | } |
429 | |
430 | void 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 | |
437 | bool 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 | |
459 | bool 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 | |
474 | bool 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 | |
504 | bool 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 | |
522 | void 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 | |
532 | std::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 | |
577 | QT_END_NAMESPACE |
578 |
Definitions
- header_size
- BitPattern
- operator==
- Indexed
- LiteralIncrementalIndexing
- LiteralNoIndexing
- LiteralNeverIndexing
- SizeUpdate
- is_literal_field
- write_bit_pattern
- read_bit_pattern
- is_request_pseudo_header
- Encoder
- dynamicTableSize
- dynamicTableCapacity
- maxDynamicTableCapacity
- encodeRequest
- encodeResponse
- encodeSizeUpdate
- setMaxDynamicTableSize
- setCompressStrings
- encodeRequestPseudoHeaders
- encodeHeaderField
- encodeMethod
- encodeResponsePseudoHeaders
- encodeIndexedField
- encodeLiteralField
- encodeLiteralField
- Decoder
- decodeHeaderFields
- dynamicTableSize
- dynamicTableCapacity
- maxDynamicTableCapacity
- setMaxDynamicTableSize
- decodeIndexedField
- decodeSizeUpdate
- decodeLiteralField
- processDecodedField
- handleStreamError
Learn Advanced QML with KDAB
Find out more