cutelyst 5.0.0
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, const 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:464
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) const const
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)
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
QString fromLatin1(QByteArrayView str)
qsizetype length() const const
CaseInsensitive