cutelyst  4.3.0
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
staticcompressed.cpp
1 /*
2  * SPDX-FileCopyrightText: (C) 2017-2023 Matthias Fehring <mf@huessenbergnetz.de>
3  * SPDX-License-Identifier: BSD-3-Clause
4  */
5 
6 #include "staticcompressed_p.h"
7 
8 #include <Cutelyst/Application>
9 #include <Cutelyst/Context>
10 #include <Cutelyst/Engine>
11 #include <Cutelyst/Request>
12 #include <Cutelyst/Response>
13 #include <array>
14 #include <chrono>
15 
16 #include <QCoreApplication>
17 #include <QCryptographicHash>
18 #include <QDataStream>
19 #include <QDateTime>
20 #include <QFile>
21 #include <QLockFile>
22 #include <QLoggingCategory>
23 #include <QMimeDatabase>
24 #include <QStandardPaths>
25 
26 #ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
27 # include <zopfli.h>
28 #endif
29 
30 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
31 # include <brotli/encode.h>
32 #endif
33 
34 using namespace Cutelyst;
35 
36 Q_LOGGING_CATEGORY(C_STATICCOMPRESSED, "cutelyst.plugin.staticcompressed", QtWarningMsg)
37 
38 StaticCompressed::StaticCompressed(Application *parent)
39  : Plugin(parent)
40  , d_ptr(new StaticCompressedPrivate)
41 {
42  Q_D(StaticCompressed);
43  d->includePaths.append(parent->config(u"root"_qs).toString());
44 }
45 
46 StaticCompressed::~StaticCompressed() = default;
47 
48 void StaticCompressed::setIncludePaths(const QStringList &paths)
49 {
50  Q_D(StaticCompressed);
51  d->includePaths.clear();
52  for (const QString &path : paths) {
53  d->includePaths.append(QDir(path));
54  }
55 }
56 
57 void StaticCompressed::setDirs(const QStringList &dirs)
58 {
59  Q_D(StaticCompressed);
60  d->dirs = dirs;
61 }
62 
63 void StaticCompressed::setServeDirsOnly(bool dirsOnly)
64 {
65  Q_D(StaticCompressed);
66  d->serveDirsOnly = dirsOnly;
67 }
68 
69 bool StaticCompressed::setup(Application *app)
70 {
71  Q_D(StaticCompressed);
72 
73  const QVariantMap config = app->engine()->config(u"Cutelyst_StaticCompressed_Plugin"_qs);
74  const QString _defaultCacheDir =
76  QLatin1String("/compressed-static");
77  d->cacheDir.setPath(config
78  .value(u"cache_directory"_qs,
79  d->defaultConfig.value(u"cache_directory"_qs, _defaultCacheDir))
80  .toString());
81 
82  if (Q_UNLIKELY(!d->cacheDir.exists())) {
83  if (!d->cacheDir.mkpath(d->cacheDir.absolutePath())) {
84  qCCritical(C_STATICCOMPRESSED)
85  << "Failed to create cache directory for compressed static files at"
86  << d->cacheDir.absolutePath();
87  return false;
88  }
89  }
90 
91  qCInfo(C_STATICCOMPRESSED) << "Compressed cache directory:" << d->cacheDir.absolutePath();
92 
93  const QString _mimeTypes =
94  config
95  .value(u"mime_types"_qs,
96  d->defaultConfig.value(u"mime_types"_qs,
97  u"text/css,application/javascript,text/javascript"_qs))
98  .toString();
99  qCInfo(C_STATICCOMPRESSED) << "MIME Types:" << _mimeTypes;
100  d->mimeTypes = _mimeTypes.split(u',', Qt::SkipEmptyParts);
101 
102  const QString _suffixes =
103  config
104  .value(
105  u"suffixes"_qs,
106  d->defaultConfig.value(u"suffixes"_qs, u"js.map,css.map,min.js.map,min.css.map"_qs))
107  .toString();
108  qCInfo(C_STATICCOMPRESSED) << "Suffixes:" << _suffixes;
109  d->suffixes = _suffixes.split(u',', Qt::SkipEmptyParts);
110 
111  d->checkPreCompressed = config
112  .value(u"check_pre_compressed"_qs,
113  d->defaultConfig.value(u"check_pre_compressed"_qs, true))
114  .toBool();
115  qCInfo(C_STATICCOMPRESSED) << "Check for pre-compressed files:" << d->checkPreCompressed;
116 
117  d->onTheFlyCompression = config
118  .value(u"on_the_fly_compression"_qs,
119  d->defaultConfig.value(u"on_the_fly_compression"_qs, true))
120  .toBool();
121  qCInfo(C_STATICCOMPRESSED) << "Compress static files on the fly:" << d->onTheFlyCompression;
122 
123  QStringList supportedCompressions{u"deflate"_qs, u"gzip"_qs};
124 
125  bool ok = false;
126  d->zlibCompressionLevel =
127  config
128  .value(u"zlib_compression_level"_qs,
129  d->defaultConfig.value(u"zlib_compression_level"_qs,
130  StaticCompressedPrivate::zlibCompressionLevelDefault))
131  .toInt(&ok);
132  if (!ok) {
133  qCWarning(C_STATICCOMPRESSED).nospace()
134  << "Invalid value set for zlib_compression_level. "
135  "Has to to be an integer value between "
136  << StaticCompressedPrivate::zlibCompressionLevelMin << " and "
137  << StaticCompressedPrivate::zlibCompressionLevelMax
138  << " inclusive. Using default value "
139  << StaticCompressedPrivate::zlibCompressionLevelDefault;
140  }
141 
142  if (d->zlibCompressionLevel < StaticCompressedPrivate::zlibCompressionLevelMin ||
143  d->zlibCompressionLevel > StaticCompressedPrivate::zlibCompressionLevelMax) {
144  qCWarning(C_STATICCOMPRESSED).nospace()
145  << "Invalid value " << d->zlibCompressionLevel
146  << " set for zlib_compression_level. Value hat to be between "
147  << StaticCompressedPrivate::zlibCompressionLevelMin << " and "
148  << StaticCompressedPrivate::zlibCompressionLevelMax
149  << " inclusive. Using default value "
150  << StaticCompressedPrivate::zlibCompressionLevelDefault;
151  d->zlibCompressionLevel = StaticCompressedPrivate::zlibCompressionLevelDefault;
152  }
153 
154 #ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
155  supportedCompressions << u"zopfli"_qs;
156  d->useZopfli =
157  config.value(u"use_zopfli"_qs, d->defaultConfig.value(u"use_zopfli"_qs, false)).toBool();
158  if (d->useZopfli) {
159  d->zopfliIterations =
160  config
161  .value(u"zopfli_iterations"_qs,
162  d->defaultConfig.value(u"zopfli_iterations"_qs,
163  StaticCompressedPrivate::zopfliIterationsDefault))
164  .toInt(&ok);
165  if (!ok) {
166  qCWarning(C_STATICCOMPRESSED).nospace()
167  << "Invalid value for zopfli_iterations. "
168  "Has to be an integer value greater than or equal to "
169  << StaticCompressedPrivate::zopfliIterationsMin << ". Using default value "
170  << StaticCompressedPrivate::zopfliIterationsDefault;
171  d->zopfliIterations = StaticCompressedPrivate::zopfliIterationsDefault;
172  }
173 
174  if (d->zopfliIterations < StaticCompressedPrivate::zopfliIterationsMin) {
175  qCWarning(C_STATICCOMPRESSED).nospace()
176  << "Invalid value " << d->zopfliIterations
177  << " set for zopfli_iterations. Value has to to be greater than or equal to "
178  << StaticCompressedPrivate::zopfliIterationsMin << ". Using default value "
179  << StaticCompressedPrivate::zopfliIterationsDefault;
180  d->zopfliIterations = StaticCompressedPrivate::zopfliIterationsDefault;
181  }
182  }
183 #endif
184 
185 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
186  d->brotliQualityLevel =
187  config
188  .value(u"brotli_quality_level"_qs,
189  d->defaultConfig.value(u"brotli_quality_level"_qs,
190  StaticCompressedPrivate::brotliQualityLevelDefault))
191  .toInt(&ok);
192  if (!ok) {
193  qCWarning(C_STATICCOMPRESSED).nospace()
194  << "Invalid value for brotli_quality_level. "
195  "Has to be an integer value between "
196  << BROTLI_MIN_QUALITY << " and " << BROTLI_MAX_QUALITY
197  << " inclusive. Using default value "
198  << StaticCompressedPrivate::brotliQualityLevelDefault;
199  d->brotliQualityLevel = StaticCompressedPrivate::brotliQualityLevelDefault;
200  }
201 
202  if (d->brotliQualityLevel < BROTLI_MIN_QUALITY || d->brotliQualityLevel > BROTLI_MAX_QUALITY) {
203  qCWarning(C_STATICCOMPRESSED).nospace()
204  << "Invalid value " << d->brotliQualityLevel
205  << " set for brotli_quality_level. Value has to be between " << BROTLI_MIN_QUALITY
206  << " and " << BROTLI_MAX_QUALITY << " inclusive. Using default value "
207  << StaticCompressedPrivate::brotliQualityLevelDefault;
208  d->brotliQualityLevel = StaticCompressedPrivate::brotliQualityLevelDefault;
209  }
210  supportedCompressions << u"brotli"_qs;
211 #endif
212 
213  qCInfo(C_STATICCOMPRESSED) << "Supported compressions:" << supportedCompressions.join(u',');
214  qCInfo(C_STATICCOMPRESSED) << "Include paths:" << d->includePaths;
215 
216  connect(app, &Application::beforePrepareAction, this, [d](Context *c, bool *skipMethod) {
217  d->beforePrepareAction(c, skipMethod);
218  });
219 
220  return true;
221 }
222 
223 void StaticCompressedPrivate::beforePrepareAction(Context *c, bool *skipMethod)
224 {
225  if (*skipMethod) {
226  return;
227  }
228 
229  // TODO mid(1) quick fix for path now having leading slash
230  const QString path = c->req()->path().mid(1);
231 
232  for (const QString &dir : dirs) {
233  if (path.startsWith(dir)) {
234  if (!locateCompressedFile(c, path)) {
235  Response *res = c->response();
236  res->setStatus(Response::NotFound);
237  res->setContentType("text/html"_qba);
238  res->setBody(u"File not found: "_qs + path);
239  }
240 
241  *skipMethod = true;
242  return;
243  }
244  }
245 
246  if (serveDirsOnly) {
247  return;
248  }
249 
250  const QRegularExpression _re = re; // Thread-safe
251  const QRegularExpressionMatch match = _re.match(path);
252  if (match.hasMatch() && locateCompressedFile(c, path)) {
253  *skipMethod = true;
254  }
255 }
256 
257 bool StaticCompressedPrivate::locateCompressedFile(Context *c, const QString &relPath) const
258 {
259  for (const QDir &includePath : includePaths) {
260  qCDebug(C_STATICCOMPRESSED)
261  << "Trying to find" << relPath << "in" << includePath.absolutePath();
262  const QString path = includePath.absoluteFilePath(relPath);
263  const QFileInfo fileInfo(path);
264  if (fileInfo.exists()) {
265  Response *res = c->res();
266  const QDateTime currentDateTime = fileInfo.lastModified();
267  if (!c->req()->headers().ifModifiedSince(currentDateTime)) {
268  res->setStatus(Response::NotModified);
269  return true;
270  }
271 
272  static QMimeDatabase db;
273  // use the extension to match to be faster
274  const QMimeType mimeType = db.mimeTypeForFile(path, QMimeDatabase::MatchExtension);
275  QByteArray contentEncoding;
276  QString compressedPath;
277  QByteArray _mimeTypeName;
278 
279  if (mimeType.isValid()) {
280 
281  // QMimeDatabase might not find the correct mime type for some specific types
282  // especially for map files for CSS and JS
283  if (mimeType.isDefault()) {
284  if (path.endsWith(u"css.map", Qt::CaseInsensitive) ||
285  path.endsWith(u"js.map", Qt::CaseInsensitive)) {
286  _mimeTypeName = "application/json"_qba;
287  }
288  }
289 
290  if (mimeTypes.contains(mimeType.name(), Qt::CaseInsensitive) ||
291  suffixes.contains(fileInfo.completeSuffix(), Qt::CaseInsensitive)) {
292 
293  const auto acceptEncoding = c->req()->header("Accept-Encoding");
294 
295 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
296  if (acceptEncoding.contains("br")) {
297  compressedPath = locateCacheFile(path, currentDateTime, Brotli);
298  if (!compressedPath.isEmpty()) {
299  qCDebug(C_STATICCOMPRESSED)
300  << "Serving brotli compressed data from" << compressedPath;
301  contentEncoding = "br"_qba;
302  }
303  } else
304 #endif
305  if (acceptEncoding.contains("gzip")) {
306  compressedPath =
307  locateCacheFile(path, currentDateTime, useZopfli ? Zopfli : Gzip);
308  if (!compressedPath.isEmpty()) {
309  qCDebug(C_STATICCOMPRESSED)
310  << "Serving" << (useZopfli ? "zopfli" : "gzip")
311  << "compressed data from" << compressedPath;
312  contentEncoding = "gzip"_qba;
313  }
314  } else if (acceptEncoding.contains("deflate")) {
315  compressedPath = locateCacheFile(path, currentDateTime, Deflate);
316  if (!compressedPath.isEmpty()) {
317  qCDebug(C_STATICCOMPRESSED)
318  << "Serving deflate compressed data from" << compressedPath;
319  contentEncoding = "deflate"_qba;
320  }
321  }
322  }
323  }
324 
325  // Response::setBody() will take the ownership
326  // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
327  QFile *file = !compressedPath.isEmpty() ? new QFile(compressedPath) : new QFile(path);
328  if (file->open(QFile::ReadOnly)) {
329  qCDebug(C_STATICCOMPRESSED) << "Serving" << path;
330  Headers &headers = res->headers();
331 
332  // set our open file
333  res->setBody(file);
334 
335  // if we have a mime type determine from the extension,
336  // do not use the name from the mime database
337  if (!_mimeTypeName.isEmpty()) {
338  headers.setContentType(_mimeTypeName);
339  } else if (mimeType.isValid()) {
340  headers.setContentType(mimeType.name().toLatin1());
341  }
342  headers.setContentLength(file->size());
343 
344  headers.setLastModified(currentDateTime);
345  // Tell Firefox & friends its OK to cache, even over SSL
346  headers.setCacheControl("public"_qba);
347 
348  if (!contentEncoding.isEmpty()) {
349  // serve correct encoding type
350  headers.setContentEncoding(contentEncoding);
351 
352  qCDebug(C_STATICCOMPRESSED)
353  << "Encoding:" << headers.contentEncoding() << "Size:" << file->size()
354  << "Original Size:" << fileInfo.size();
355 
356  // force proxies to cache compressed and non-compressed files separately
357  headers.pushHeader("Vary"_qba, "Accept-Encoding"_qba);
358  }
359 
360  return true;
361  }
362 
363  qCWarning(C_STATICCOMPRESSED) << "Could not serve" << path << file->errorString();
364  delete file;
365  return false;
366  }
367  }
368 
369  qCWarning(C_STATICCOMPRESSED) << "File not found" << relPath;
370  return false;
371 }
372 
373 QString StaticCompressedPrivate::locateCacheFile(const QString &origPath,
374  const QDateTime &origLastModified,
375  Compression compression) const
376 {
377  QString compressedPath;
378 
379  QString suffix;
380 
381  switch (compression) {
382  case Zopfli:
383  case Gzip:
384  suffix = u".gz"_qs;
385  break;
386 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
387  case Brotli:
388  suffix = u".br"_qs;
389  break;
390 #endif
391  case Deflate:
392  suffix = u".deflate"_qs;
393  break;
394  default:
395  Q_ASSERT_X(false, "locate cache file", "invalid compression type");
396  break;
397  }
398 
399  if (checkPreCompressed) {
400  const QFileInfo origCompressed(origPath + suffix);
401  if (origCompressed.exists()) {
402  compressedPath = origCompressed.absoluteFilePath();
403  return compressedPath;
404  }
405  }
406 
407  if (onTheFlyCompression) {
408 
409  const QString path = cacheDir.absoluteFilePath(
412  suffix);
413  const QFileInfo info(path);
414 
415  if (info.exists() && (info.lastModified() > origLastModified)) {
416  compressedPath = path;
417  } else {
418  QLockFile lock(path + QLatin1String(".lock"));
419  if (lock.tryLock(std::chrono::milliseconds{10})) {
420  switch (compression) {
421 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
422  case Brotli:
423  if (compressBrotli(origPath, path)) {
424  compressedPath = path;
425  }
426  break;
427 #endif
428  case Zopfli:
429 #ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
430  if (compressZopfli(origPath, path)) {
431  compressedPath = path;
432  }
433  break;
434 #endif
435  case Gzip:
436  if (compressGzip(origPath, path, origLastModified)) {
437  compressedPath = path;
438  }
439  break;
440  case Deflate:
441  if (compressDeflate(origPath, path)) {
442  compressedPath = path;
443  }
444  break;
445  default:
446  break;
447  }
448  lock.unlock();
449  }
450  }
451  }
452 
453  return compressedPath;
454 }
455 
456 // clang-format off
457 static constexpr std::array<quint32, 256> crc_32_tab { /* CRC polynomial 0xedb88320 */
458  0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
459  0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
460  0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
461  0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
462  0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
463  0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
464  0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
465  0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
466  0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
467  0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
468  0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
469  0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
470  0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
471  0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
472  0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
473  0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
474  0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
475  0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
476  0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
477  0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
478  0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
479  0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
480  0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
481  0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
482  0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
483  0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
484  0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
485  0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
486  0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
487  0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
488  0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
489  0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
490  0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
491  0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
492  0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
493  0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
494  0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
495  0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
496  0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
497  0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
498  0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
499  0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
500  0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
501 };
502 // clang-format on
503 
504 quint32 updateCRC32(unsigned char ch, quint32 crc)
505 {
506  // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers)
507  return (crc_32_tab[((crc) ^ (quint8(ch))) & 0xff] ^ ((crc) >> 8));
508 }
509 
510 quint32 crc32buf(const QByteArray &data)
511 {
512  return ~std::accumulate(data.begin(),
513  data.end(),
514  quint32(0xFFFFFFFF), // NOLINT(cppcoreguidelines-avoid-magic-numbers)
515  [](quint32 oldcrc32, char buf) { return updateCRC32(buf, oldcrc32); });
516 }
517 
518 bool StaticCompressedPrivate::compressGzip(const QString &inputPath,
519  const QString &outputPath,
520  const QDateTime &origLastModified) const
521 {
522  qCDebug(C_STATICCOMPRESSED) << "Compressing" << inputPath << "with gzip to" << outputPath;
523 
524  QFile input(inputPath);
525  if (Q_UNLIKELY(!input.open(QIODevice::ReadOnly))) {
526  qCWarning(C_STATICCOMPRESSED)
527  << "Can not open input file to compress with gzip:" << inputPath;
528  return false;
529  }
530 
531  const QByteArray data = input.readAll();
532  if (Q_UNLIKELY(data.isEmpty())) {
533  qCWarning(C_STATICCOMPRESSED)
534  << "Can not read input file or input file is empty:" << inputPath;
535  input.close();
536  return false;
537  }
538 
539  QByteArray compressedData = qCompress(data, zlibCompressionLevel);
540  input.close();
541 
542  QFile output(outputPath);
543  if (Q_UNLIKELY(!output.open(QIODevice::WriteOnly))) {
544  qCWarning(C_STATICCOMPRESSED)
545  << "Can not open output file to compress with gzip:" << outputPath;
546  return false;
547  }
548 
549  if (Q_UNLIKELY(compressedData.isEmpty())) {
550  qCWarning(C_STATICCOMPRESSED)
551  << "Failed to compress file with gzip, compressed data is empty:" << inputPath;
552  if (output.exists()) {
553  if (Q_UNLIKELY(!output.remove())) {
554  qCWarning(C_STATICCOMPRESSED)
555  << "Can not remove invalid compressed gzip file:" << outputPath;
556  }
557  }
558  return false;
559  }
560 
561  // Strip the first six bytes (a 4-byte length put on by qCompress and a 2-byte zlib header)
562  // and the last four bytes (a zlib integrity check).
563  compressedData.remove(0, 6);
564  compressedData.chop(4);
565 
566  QByteArray header;
567  QDataStream headerStream(&header, QIODevice::WriteOnly);
568  // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers)
569  // prepend a generic 10-byte gzip header (see RFC 1952)
570  headerStream << quint16(0x1f8b) << quint16(0x0800)
571  << quint32(origLastModified.toSecsSinceEpoch())
572 #if defined Q_OS_UNIX
573  << quint16(0x0003);
574 #elif defined Q_OS_WIN
575  << quint16(0x000b);
576 #elif defined Q_OS_MACOS
577  << quint16(0x0007);
578 #else
579  << quint16(0x00ff);
580 #endif
581  // NOLINTEND(cppcoreguidelines-avoid-magic-numbers)
582 
583  // append a four-byte CRC-32 of the uncompressed data
584  // append 4 bytes uncompressed input size modulo 2^32
585  QByteArray footer;
586  QDataStream footerStream(&footer, QIODevice::WriteOnly);
587  footerStream << crc32buf(data) << quint32(data.size());
588 
589  if (Q_UNLIKELY(output.write(header + compressedData + footer) < 0)) {
590  qCCritical(C_STATICCOMPRESSED).nospace()
591  << "Failed to write compressed gzip file " << inputPath << ": " << output.errorString();
592  return false;
593  }
594 
595  return true;
596 }
597 
598 bool StaticCompressedPrivate::compressDeflate(const QString &inputPath,
599  const QString &outputPath) const
600 {
601  qCDebug(C_STATICCOMPRESSED) << "Compressing" << inputPath << "with deflate to" << outputPath;
602 
603  QFile input(inputPath);
604  if (Q_UNLIKELY(!input.open(QIODevice::ReadOnly))) {
605  qCWarning(C_STATICCOMPRESSED)
606  << "Can not open input file to compress with deflate:" << inputPath;
607  return false;
608  }
609 
610  const QByteArray data = input.readAll();
611  if (Q_UNLIKELY(data.isEmpty())) {
612  qCWarning(C_STATICCOMPRESSED)
613  << "Can not read input file or input file is empty:" << inputPath;
614  input.close();
615  return false;
616  }
617 
618  QByteArray compressedData = qCompress(data, zlibCompressionLevel);
619  input.close();
620 
621  QFile output(outputPath);
622  if (Q_UNLIKELY(!output.open(QIODevice::WriteOnly))) {
623  qCWarning(C_STATICCOMPRESSED)
624  << "Can not open output file to compress with deflate:" << outputPath;
625  return false;
626  }
627 
628  if (Q_UNLIKELY(compressedData.isEmpty())) {
629  qCWarning(C_STATICCOMPRESSED)
630  << "Failed to compress file with deflate, compressed data is empty:" << inputPath;
631  if (output.exists()) {
632  if (Q_UNLIKELY(!output.remove())) {
633  qCWarning(C_STATICCOMPRESSED)
634  << "Can not remove invalid compressed deflate file:" << outputPath;
635  }
636  }
637  return false;
638  }
639 
640  // Strip the first six bytes (a 4-byte length put on by qCompress and a 2-byte zlib header)
641  // and the last four bytes (a zlib integrity check).
642  compressedData.remove(0, 6);
643  compressedData.chop(4);
644 
645  if (Q_UNLIKELY(output.write(compressedData) < 0)) {
646  qCCritical(C_STATICCOMPRESSED).nospace() << "Failed to write compressed deflate file "
647  << inputPath << ": " << output.errorString();
648  return false;
649  }
650 
651  return true;
652 }
653 
654 #ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
655 bool StaticCompressedPrivate::compressZopfli(const QString &inputPath,
656  const QString &outputPath) const
657 {
658  qCDebug(C_STATICCOMPRESSED) << "Compressing" << inputPath << "with zopfli to" << outputPath;
659 
660  QFile input(inputPath);
661  if (Q_UNLIKELY(!input.open(QIODevice::ReadOnly))) {
662  qCWarning(C_STATICCOMPRESSED)
663  << "Can not open input file to compress with zopfli:" << inputPath;
664  return false;
665  }
666 
667  const QByteArray data = input.readAll();
668  if (Q_UNLIKELY(data.isEmpty())) {
669  qCWarning(C_STATICCOMPRESSED)
670  << "Can not read input file or input file is empty:" << inputPath;
671  input.close();
672  return false;
673  }
674 
675  ZopfliOptions options;
676  ZopfliInitOptions(&options);
677  options.numiterations = zopfliIterations;
678 
679  unsigned char *out{nullptr};
680  size_t outSize{0};
681 
682  ZopfliCompress(&options,
683  ZopfliFormat::ZOPFLI_FORMAT_GZIP,
684  reinterpret_cast<const unsigned char *>(data.constData()),
685  data.size(),
686  &out,
687  &outSize);
688 
689  bool ok = false;
690  if (outSize > 0) {
691  QFile output(outputPath);
692  if (Q_UNLIKELY(!output.open(QIODevice::WriteOnly))) {
693  qCWarning(C_STATICCOMPRESSED)
694  << "Can not open output file to compress with zopfli:" << outputPath;
695  } else {
696  if (Q_UNLIKELY(output.write(reinterpret_cast<const char *>(out), outSize) < 0)) {
697  qCCritical(C_STATICCOMPRESSED).nospace()
698  << "Failed to write compressed zopfi file " << inputPath << ": "
699  << output.errorString();
700  if (output.exists()) {
701  if (Q_UNLIKELY(!output.remove())) {
702  qCWarning(C_STATICCOMPRESSED)
703  << "Can not remove invalid compressed zopfli file:" << outputPath;
704  }
705  }
706  } else {
707  ok = true;
708  }
709  }
710  } else {
711  qCWarning(C_STATICCOMPRESSED)
712  << "Failed to compress file with zopfli, compressed data is empty:" << inputPath;
713  }
714 
715  free(out);
716 
717  return ok;
718 }
719 #endif
720 
721 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
722 bool StaticCompressedPrivate::compressBrotli(const QString &inputPath,
723  const QString &outputPath) const
724 {
725  qCDebug(C_STATICCOMPRESSED) << "Compressing" << inputPath << "with brotli to" << outputPath;
726 
727  QFile input(inputPath);
728  if (Q_UNLIKELY(!input.open(QIODevice::ReadOnly))) {
729  qCWarning(C_STATICCOMPRESSED)
730  << "Can not open input file to compress with brotli:" << inputPath;
731  return false;
732  }
733 
734  const QByteArray data = input.readAll();
735  if (Q_UNLIKELY(data.isEmpty())) {
736  qCWarning(C_STATICCOMPRESSED)
737  << "Can not read input file or input file is empty:" << inputPath;
738  return false;
739  }
740 
741  input.close();
742 
743  bool ok = false;
744 
745  size_t outSize = BrotliEncoderMaxCompressedSize(static_cast<size_t>(data.size()));
746  if (Q_LIKELY(outSize > 0)) {
747  const auto in = reinterpret_cast<const uint8_t *>(data.constData());
748  auto out = static_cast<uint8_t *>(malloc(sizeof(uint8_t) * (outSize + 1)));
749  if (Q_LIKELY(out != nullptr)) {
750  BROTLI_BOOL status = BrotliEncoderCompress(brotliQualityLevel,
751  BROTLI_DEFAULT_WINDOW,
752  BROTLI_DEFAULT_MODE,
753  data.size(),
754  in,
755  &outSize,
756  out);
757  if (Q_LIKELY(status == BROTLI_TRUE)) {
758  QFile output(outputPath);
759  if (Q_LIKELY(output.open(QIODevice::WriteOnly))) {
760  if (Q_LIKELY(output.write(reinterpret_cast<const char *>(out), outSize) > -1)) {
761  ok = true;
762  } else {
763  qCWarning(C_STATICCOMPRESSED).nospace()
764  << "Failed to write brotli compressed data to output file "
765  << outputPath << ": " << output.errorString();
766  if (output.exists()) {
767  if (Q_UNLIKELY(!output.remove())) {
768  qCWarning(C_STATICCOMPRESSED)
769  << "Can not remove invalid compressed brotli file:"
770  << outputPath;
771  }
772  }
773  }
774  } else {
775  qCWarning(C_STATICCOMPRESSED)
776  << "Failed to open output file for brotli compression:" << outputPath;
777  }
778  } else {
779  qCWarning(C_STATICCOMPRESSED) << "Failed to compress" << inputPath << "with brotli";
780  }
781  free(out);
782  } else {
783  qCWarning(C_STATICCOMPRESSED)
784  << "Can not allocate needed output buffer of size"
785  << (sizeof(uint8_t) * (outSize + 1)) << "for brotli compression.";
786  }
787  } else {
788  qCWarning(C_STATICCOMPRESSED) << "Needed output buffer too large to compress input of size"
789  << data.size() << "with brotli";
790  }
791 
792  return ok;
793 }
794 #endif
795 
796 #include "moc_staticcompressed.cpp"
QByteArray header(QByteArrayView key) const noexcept
Definition: request.h:611
QRegularExpressionMatch match(const QString &subject, qsizetype offset, QRegularExpression::MatchType matchType, QRegularExpression::MatchOptions matchOptions) const const
QString writableLocation(QStandardPaths::StandardLocation type)
Serve static files compressed on the fly or pre-compressed.
Headers & headers() noexcept
QString errorString() const const
QStringList split(const QString &sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
Response * res() const noexcept
Definition: context.cpp:103
void chop(qsizetype n)
void setContentEncoding(const QByteArray &encoding)
Definition: headers.cpp:62
bool isEmpty() const const
Container for HTTP headers.
Definition: headers.h:23
void setLastModified(const QByteArray &value)
Definition: headers.cpp:271
STL namespace.
Request req
Definition: context.h:67
void setContentType(const QByteArray &type)
Definition: response.h:238
T value(qsizetype i) const const
QMimeType mimeTypeForFile(const QString &fileName, QMimeDatabase::MatchMode mode) const const
A Cutelyst response.
Definition: response.h:28
void setCacheControl(const QByteArray &value)
Definition: headers.cpp:38
The Cutelyst Context.
Definition: context.h:42
void setContentType(const QByteArray &contentType)
Definition: headers.cpp:76
Headers headers() const noexcept
Definition: request.cpp:312
CaseInsensitive
void beforePrepareAction(Cutelyst::Context *c, bool *skipMethod)
QVariantMap config(const QString &entity) const
Definition: engine.cpp:263
bool isEmpty() const const
const char * constData() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QByteArray::iterator begin()
bool hasMatch() const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
The Cutelyst namespace holds all public Cutelyst API.
virtual bool open(QIODeviceBase::OpenMode mode) override
SkipEmptyParts
virtual qint64 size() const const override
bool isValid() const const
QByteArray ifModifiedSince() const noexcept
Definition: headers.cpp:205
void pushHeader(const QByteArray &key, const QByteArray &value)
Definition: headers.cpp:458
QByteArray contentEncoding() const noexcept
Definition: headers.cpp:57
QString fromLatin1(QByteArrayView str)
QString mid(qsizetype position, qsizetype n) const const
qint64 toSecsSinceEpoch() const const
QByteArray hash(const QByteArray &data, QCryptographicHash::Algorithm method)
Base class for Cutelyst Plugins.
Definition: plugin.h:24
The Cutelyst application.
Definition: application.h:72
Engine * engine() const noexcept
void setBody(QIODevice *body)
Definition: response.cpp:103
void setContentLength(qint64 value)
Definition: headers.cpp:172
qsizetype size() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QObject * parent() const const
Response * response() const noexcept
Definition: context.cpp:97
QByteArray & remove(qsizetype pos, qsizetype len)
void setStatus(quint16 status) noexcept
Definition: response.cpp:72
QByteArray::iterator end()
QByteArray toUtf8() const const