cutelyst 4.8.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:461
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