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 
17 using namespace Cutelyst;
18 
19 namespace {
20 unsigned 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
71 unsigned 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 
93 void 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 
109 inline 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 
124 unsigned 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 
151 unsigned 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 
184 bool 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 
225 bool validHeader(const QByteArray &k, const QByteArray &v)
226 {
227  return k.compare("connection") != 0 && (k.compare("te") != 0 || v.compare("trailers") == 0);
228 }
229 
230 void 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 
239 HPack::HPack(int maxTableSize)
240  : m_currentMaxDynamicTableSize(maxTableSize)
241  , m_maxTableSize(maxTableSize)
242 {
243 }
244 
245 HPack::~HPack()
246 {
247 }
248 
249 void 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 
309 enum 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 
326 int 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 }
bool isUpper(char32_t ucs4)
void reserve(qsizetype size)
char at(qsizetype i) const const
void setPath(char *rawPath, int len)
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:420
QByteArray mid(qsizetype pos, qsizetype len) const const
QByteArray & append(QByteArrayView data)
qlonglong toLongLong(bool *ok, int base) const const
void pushHeader(const QByteArray &key, const QByteArray &value)
Definition: headers.cpp:489
QString fromLatin1(QByteArrayView str)
qsizetype length() const const
qsizetype size() const const