cutelyst 5.0.1
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
hpack.cpp
1/*
2 * SPDX-FileCopyrightText: (C) 2018 Daniel Nicoletti <dantti12@gmail.com>
3 * SPDX-License-Identifier: BSD-3-Clause
4 */
5#include "hpack.h"
6
7#include "hpack_p.h"
8#include "protocolhttp2.h"
9#include "serverengine.h"
10
11#include <vector>
12
13#include <QDebug>
14
15#define INT_MASK(bits) ((1 << (bits)) - 1)
16
17using namespace Cutelyst;
18
19namespace {
20unsigned char *
21 hpackDecodeString(unsigned char *src, unsigned char *src_end, QByteArray &value, int len)
22{
23 quint8 state = 0;
24 const HPackPrivate::HuffDecode *entry = nullptr;
25 value.reserve(len * 2); // max compression ratio is >= 0.5
26
27 do {
28 if (entry) {
29 state = entry->state;
30 }
31 entry = HPackPrivate::huff_decode_table[state] + (*src >> 4);
32
33 if (entry->flags & HPackPrivate::HUFF_FAIL) {
34 // A decoder decoded an invalid Huffman sequence
35 return nullptr;
36 }
37
38 if (entry->flags & HPackPrivate::HUFF_SYM) {
39 value.append(char(entry->sym));
40 }
41
42 entry = HPackPrivate::huff_decode_table[entry->state] + (*src & 0x0f);
43
44 if (entry->flags & HPackPrivate::HUFF_FAIL) {
45 // A decoder decoded an invalid Huffman sequence
46 return nullptr;
47 }
48
49 if ((entry->flags & HPackPrivate::HUFF_SYM) != 0) {
50 value.append(char(entry->sym));
51 }
52
53 } while (++src < src_end);
54
55 // qDebug() << "maybe_eos = " << ((entry->flags & HPackPrivate::HUFF_ACCEPTED) != 0) <<
56 // "entry->state =" << entry->state;
57
58 if ((entry->flags & HPackPrivate::HUFF_ACCEPTED) == 0) {
59 // entry->state == 28 // A invalid header name or value character was coded
60 // entry->state != 28 // A decoder decoded an invalid Huffman sequence
61 return nullptr;
62 }
63
64 return src_end;
65}
66
67// This decodes an UInt
68// it returns nullptr if it tries to read past end
69// The value can overflow it's capacity, which should be harmless as it would
70// give parsing errors on other parts
71unsigned char *
72 decodeUInt16(unsigned char *src, const unsigned char *src_end, quint16 &dst, quint8 mask)
73{
74 // qDebug() << Q_FUNC_INFO << "value 0" << QByteArray((char*)src, 10).toHex();
75
76 dst = *src & mask;
77 if (dst == mask) {
78 int M = 0;
79 do {
80 if (++src >= src_end) {
81 dst = quint16(-1);
82 return nullptr;
83 }
84
85 dst += (*src & 0x7f) << M;
86 M += 7;
87 } while (*src & 0x80);
88 }
89
90 return ++src;
91}
92
93void encodeUInt16(QByteArray &buf, int I, quint8 mask)
94{
95 if (I < mask) {
96 buf.append(char(I));
97 return;
98 }
99
100 I -= mask;
101 buf.append(char(mask));
102 while (I >= 128) {
103 buf.append(char((I & 0x7f) | 0x80));
104 I = I >> 7;
105 }
106 buf.append(char(I));
107}
108
109inline void encodeH2caseHeader(QByteArray &buf, const QString &key)
110{
111
112 encodeUInt16(buf, key.length(), INT_MASK(7));
113 for (auto keyIt : key) {
114 if (keyIt.isLetter()) {
115 buf.append(keyIt.toLower().toLatin1());
116 } else if (keyIt == u'_') {
117 buf.append('-');
118 } else {
119 buf.append(keyIt.toLatin1());
120 }
121 }
122}
123
124unsigned char *parse_string(QByteArray &dst, unsigned char *buf, const quint8 *itEnd)
125{
126 quint16 str_len = 0;
127
128 bool huffmanDecode = *buf & 0x80;
129
130 buf = decodeUInt16(buf, itEnd, str_len, INT_MASK(7));
131 if (!buf) {
132 return nullptr;
133 }
134
135 if (huffmanDecode) {
136 buf = hpackDecodeString(buf, buf + str_len, dst, str_len);
137 if (!buf) {
138 return nullptr;
139 }
140 } else {
141 if (buf + str_len <= itEnd) {
142 dst = QByteArray(reinterpret_cast<const char *>(buf), str_len);
143 buf += str_len;
144 } else {
145 return nullptr; // Reading past end
146 }
147 }
148 return buf;
149}
150
151unsigned char *parse_string_key(QByteArray &dst, quint8 *buf, const quint8 *itEnd)
152{
153 quint16 str_len = 0;
154 bool huffmanDecode = *buf & 0x80;
155
156 buf = decodeUInt16(buf, itEnd, str_len, INT_MASK(7));
157 if (!buf) {
158 return nullptr;
159 }
160
161 if (huffmanDecode) {
162 buf = hpackDecodeString(buf, buf + str_len, dst, str_len);
163 if (!buf) {
164 return nullptr;
165 }
166 } else {
167 if (buf + str_len <= itEnd) {
168 itEnd = buf + str_len;
169
170 while (buf < itEnd) {
171 QChar c = QLatin1Char(char(*(buf++)));
172 if (c.isUpper()) {
173 return nullptr;
174 }
175 dst += char(*(buf++));
176 }
177 } else {
178 return nullptr; // Reading past end
179 }
180 }
181 return buf;
182}
183
184bool validPseudoHeader(const QByteArray &k, const QByteArray &v, H2Stream *stream)
185{
186 // qDebug() << "validPseudoHeader" << k << v << stream->path << stream->method <<
187 // stream->scheme;
188 if (k.compare(":path") == 0) {
189 if (!stream->gotPath && !v.isEmpty()) {
190 int leadingSlash = 0;
191 while (leadingSlash < v.size() && v.at(leadingSlash) == u'/') {
192 ++leadingSlash;
193 }
194
195 int pos = v.indexOf('?');
196 if (pos == -1) {
197 QByteArray path = v.mid(leadingSlash);
198 stream->setPath(path);
199 } else {
200 QByteArray path = v.mid(leadingSlash, pos - leadingSlash);
201 stream->setPath(path);
202 stream->query = v.mid(++pos);
203 }
204 stream->gotPath = true;
205 return true;
206 }
207 } else if (k.compare(":method") == 0) {
208 if (stream->method.isEmpty()) {
209 stream->method = v;
210 return true;
211 }
212 } else if (k.compare(":authority") == 0) {
213 stream->serverAddress = v;
214 return true;
215 } else if (k.compare(":scheme") == 0) {
216 if (stream->scheme.isEmpty()) {
217 stream->scheme = v;
218 stream->isSecure = v.compare("https") == 0;
219 return true;
220 }
221 }
222 return false;
223}
224
225bool validHeader(const QByteArray &k, const QByteArray &v)
226{
227 return k.compare("connection") != 0 && (k.compare("te") != 0 || v.compare("trailers") == 0);
228}
229
230void consumeHeader(const QByteArray &k, const QByteArray &v, H2Stream *stream)
231{
232 if (k.compare("content-length") == 0) {
233 stream->contentLength = v.toLongLong();
234 }
235}
236
237} // namespace
238
239HPack::HPack(int maxTableSize)
240 : m_currentMaxDynamicTableSize(maxTableSize)
241 , m_maxTableSize(maxTableSize)
242{
243}
244
245HPack::~HPack()
246{
247}
248
249void HPack::encodeHeaders(int status, const Headers &headers, QByteArray &buf, ServerEngine *engine)
250{
251 if (status == 200) {
252 buf.append(char(0x88));
253 } else if (status == 204) {
254 buf.append(char(0x89));
255 } else if (status == 206) {
256 buf.append(char(0x8A));
257 } else if (status == 304) {
258 buf.append(char(0x8B));
259 } else if (status == 400) {
260 buf.append(char(0x8C));
261 } else if (status == 404) {
262 buf.append(char(0x8D));
263 } else if (status == 500) {
264 buf.append(char(0x8E));
265 } else {
266 buf.append(char(0x08));
267
268 const QByteArray statusStr = QByteArray::number(status);
269 encodeUInt16(buf, statusStr.length(), INT_MASK(4));
270 buf.append(statusStr);
271 }
272
273 bool hasDate = false;
274 const auto headersData = headers.data();
275 for (const auto &[key, value] : headersData) {
276 if (!hasDate && key.compare("Date", Qt::CaseInsensitive) == 0) {
277 hasDate = true;
278 }
279
280 auto staticIt = HPackPrivate::hpackStaticHeadersCode.constFind(key);
281 if (staticIt != HPackPrivate::hpackStaticHeadersCode.constEnd()) {
282 buf.append(staticIt.value(), 2);
283
284 encodeUInt16(buf, value.length(), INT_MASK(7));
285 buf.append(value);
286 } else {
287 buf.append('\x00');
288 encodeH2caseHeader(buf, QString::fromLatin1(key));
289
290 encodeUInt16(buf, value.length(), INT_MASK(7));
291 buf.append(value);
292 }
293 }
294
295 if (!hasDate) {
296 const QByteArray date = engine->lastDate().mid(8);
297 if (date.length() != 29) {
298 // This should never happen but...
299 return;
300 }
301
302 // 0f12 Date header not indexed
303 // 1d = date length: 29
304 buf.append("\x0f\x12\x1d", 3);
305 buf.append(date);
306 }
307}
308
309enum ErrorCodes {
310 ErrorNoError = 0x0,
311 ErrorProtocolError = 0x1,
312 ErrorInternalError = 0x2,
313 ErrorFlowControlError = 0x3,
314 ErrorSettingsTimeout = 0x4,
315 ErrorStreamClosed = 0x5,
316 ErrorFrameSizeError = 0x6,
317 ErrorRefusedStream = 0x7,
318 ErrorCancel = 0x8,
319 ErrorCompressionError = 0x9,
320 ErrorConnectError = 0xA,
321 ErrorEnhanceYourCalm = 0xB,
322 ErrorInadequateSecurity = 0xC,
323 ErrorHttp11Required = 0xD
324};
325
326int HPack::decode(unsigned char *it, const unsigned char *itEnd, H2Stream *stream)
327{
328 bool pseudoHeadersAllowed = true;
329 bool allowedToUpdate = true;
330 while (it < itEnd) {
331 quint16 intValue(0);
332 if (*it & 0x80) {
333 it = decodeUInt16(it, itEnd, intValue, INT_MASK(7));
334 // qDebug() << "6.1 Indexed Header Field Representation" << *it << intValue
335 // << it;
336 if (!it || intValue == 0) {
337 return ErrorCompressionError;
338 }
339
340 QByteArray key;
341 QByteArray value;
342 if (intValue > 61) {
343 // qDebug() << "6.1 Indexed Header Field Representation dynamic table
344 // lookup" << *it << intValue << m_dynamicTable.size();
345 intValue -= 62;
346 if (intValue < qint64(m_dynamicTable.size())) {
347 const auto h = m_dynamicTable[intValue];
348 key = h.key;
349 value = h.value;
350 } else {
351 return ErrorCompressionError;
352 }
353 } else {
354 const auto h = HPackPrivate::hpackStaticHeaders[intValue];
355 key = h.key;
356 value = h.value;
357 }
358
359 // qDebug() << "header" << key << value;
360 if (key.startsWith(':')) {
361 if (!pseudoHeadersAllowed || !validPseudoHeader(key, value, stream)) {
362 return ErrorProtocolError;
363 }
364 } else {
365 if (!validHeader(key, value)) {
366 return ErrorProtocolError;
367 }
368 pseudoHeadersAllowed = false;
369 consumeHeader(key, value, stream);
370 stream->headers.pushHeader(key, value);
371 }
372 } else {
373 bool addToDynamicTable = false;
374 if (*it & 0x40) {
375 // 6.2.1 Literal Header Field with Incremental Indexing
376 it = decodeUInt16(it, itEnd, intValue, INT_MASK(6));
377 if (!it) {
378 return ErrorCompressionError;
379 }
380 addToDynamicTable = true;
381 // qDebug() << "6.2.1 Literal Header Field" << *it << "value" <<
382 // intValue << "allowedToUpdate" << allowedToUpdate;
383 } else if (*it & 0x20) {
384 it = decodeUInt16(it, itEnd, intValue, INT_MASK(5));
385 // qDebug() << "6.3 Dynamic Table update" << *it << "value" <<
386 // intValue << "allowedToUpdate" << allowedToUpdate <<
387 // m_maxTableSize;
388 if (!it || intValue > m_maxTableSize || !allowedToUpdate) {
389 return ErrorCompressionError;
390 }
391
392 m_currentMaxDynamicTableSize = intValue;
393 while (m_dynamicTableSize > m_currentMaxDynamicTableSize &&
394 !m_dynamicTable.empty()) {
395 auto header = m_dynamicTable.takeLast();
396 m_dynamicTableSize -= header.key.length() + header.value.length() + 32;
397 }
398
399 continue;
400 } else {
401 // 6.2.2 Literal Header Field without Indexing
402 // 6.2.3 Literal Header Field Never Indexed
403 it = decodeUInt16(it, itEnd, intValue, INT_MASK(4));
404 if (!it) {
405 return ErrorCompressionError;
406 }
407 }
408
409 QByteArray key;
410 if (intValue > 61) {
411 if (addToDynamicTable) {
412 // 6.2.1 Literal Header Field with Incremental Indexing
413 // Indexed Name
414 if (intValue - 62 < qint64(m_dynamicTable.size())) {
415 const auto h = m_dynamicTable[intValue - 62];
416 key = h.key;
417 } else {
418 return ErrorCompressionError;
419 }
420 } else {
421 return ErrorCompressionError;
422 }
423 } else if (intValue != 0) {
424 const auto h = HPackPrivate::hpackStaticHeaders[intValue];
425 key = h.key;
426 } else {
427 it = parse_string_key(key, it, itEnd);
428 if (!it) {
429 return ErrorProtocolError;
430 }
431 }
432
433 QByteArray value;
434 it = parse_string(value, it, itEnd);
435 if (!it) {
436 return ErrorCompressionError;
437 }
438
439 if (key.startsWith(':')) {
440 if (!pseudoHeadersAllowed || !validPseudoHeader(key, value, stream)) {
441 return ErrorProtocolError;
442 }
443 } else {
444 if (!validHeader(key, value)) {
445 return ErrorProtocolError;
446 }
447 pseudoHeadersAllowed = false;
448 consumeHeader(key, value, stream);
449 stream->headers.pushHeader(key, value);
450 }
451
452 if (addToDynamicTable) {
453 const int size = key.length() + value.length() + 32;
454 while (size + m_dynamicTableSize > m_currentMaxDynamicTableSize &&
455 !m_dynamicTable.empty()) {
456 const DynamicTableEntry entry = m_dynamicTable.takeLast();
457 m_dynamicTableSize -= entry.key.length() + entry.value.length() + 32;
458 }
459
460 if (size + m_dynamicTableSize <= m_currentMaxDynamicTableSize) {
461 m_dynamicTable.prepend({key, value});
462 m_dynamicTableSize += size;
463 }
464 }
465
466 // qDebug() << "header key/value" << key << value;
467 }
468
469 allowedToUpdate = false;
470 }
471
472 if (!stream->gotPath || stream->method.isEmpty() || stream->scheme.isEmpty()) {
473 return ErrorProtocolError;
474 }
475
476 return 0;
477}
void setPath(char *rawPath, int len)
Container for HTTP headers.
Definition headers.h:24
QVector< HeaderKeyValue > data() const
Definition headers.h:420
void pushHeader(const QByteArray &key, const QByteArray &value)
Definition headers.cpp:489
The Cutelyst namespace holds all public Cutelyst API.
QByteArray & append(QByteArrayView data)
char at(qsizetype i) const const
int compare(QByteArrayView bv, Qt::CaseSensitivity cs) const const
qsizetype indexOf(QByteArrayView bv, qsizetype from) const const
bool isEmpty() const const
qsizetype length() const const
QByteArray mid(qsizetype pos, qsizetype len) &&
QByteArray number(double n, char format, int precision)
qsizetype size() const const
bool startsWith(QByteArrayView bv) const const
qlonglong toLongLong(bool *ok, int base) const const
bool isUpper(char32_t ucs4)
QString fromLatin1(QByteArrayView str)
qsizetype length() const const
CaseInsensitive