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