cutelyst  4.9.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 
17 using namespace Cutelyst;
18 
19 unsigned 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
26 unsigned 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 
47 void 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 
63 static 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 
78 unsigned 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 
105 unsigned 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 
138 HPack::HPack(int maxTableSize)
139  : m_currentMaxDynamicTableSize(maxTableSize)
140  , m_maxTableSize(maxTableSize)
141 {
142 }
143 
144 HPack::~HPack()
145 {
146 }
147 
148 void 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 
211 enum 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 
228 inline 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 
269 inline 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 
274 inline 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 
281 int 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 
434 unsigned 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 }
bool isUpper(char32_t ucs4)
char at(qsizetype i) const const
bool isEmpty() const const
bool startsWith(QByteArrayView bv) const const
Container for HTTP headers.
Definition: headers.h:23
qsizetype length() const const
int compare(QByteArrayView bv, Qt::CaseSensitivity cs) const const
qsizetype indexOf(QByteArrayView bv, qsizetype from) const const
QByteArray number(double n, char format, int precision)
CaseInsensitive
The Cutelyst namespace holds all public Cutelyst API.
QVector< HeaderKeyValue > data() const
Definition: headers.h:419
QByteArray mid(qsizetype pos, qsizetype len) const const
void setPath(char *rawPath, const int len)
QByteArray & append(QByteArrayView data)
qlonglong toLongLong(bool *ok, int base) const const
void pushHeader(const QByteArray &key, const QByteArray &value)
Definition: headers.cpp:461
QString fromLatin1(QByteArrayView str)
qsizetype length() const const
qsizetype size() const const