cutelyst  4.3.0
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
request.cpp
1 /*
2  * SPDX-FileCopyrightText: (C) 2013-2022 Daniel Nicoletti <dantti12@gmail.com>
3  * SPDX-License-Identifier: BSD-3-Clause
4  */
5 #include "common.h"
6 #include "engine.h"
7 #include "enginerequest.h"
8 #include "multipartformdataparser.h"
9 #include "request_p.h"
10 #include "utils.h"
11 
12 #include <QHostInfo>
13 #include <QJsonArray>
14 #include <QJsonDocument>
15 #include <QJsonObject>
16 
17 using namespace Cutelyst;
18 
20  : d_ptr(new RequestPrivate)
21 {
22  d_ptr->engineRequest = engineRequest;
23  d_ptr->body = engineRequest->body;
24 }
25 
27 {
28  qDeleteAll(d_ptr->uploads);
29  delete d_ptr->body;
30  delete d_ptr;
31 }
32 
34 {
35  Q_D(const Request);
36  return d->engineRequest->remoteAddress;
37 }
38 
40 {
41  Q_D(const Request);
42 
43  bool ok;
44  quint32 data = d->engineRequest->remoteAddress.toIPv4Address(&ok);
45  if (ok) {
46  return QHostAddress(data).toString();
47  } else {
48  return d->engineRequest->remoteAddress.toString();
49  }
50 }
51 
53 {
54  Q_D(const Request);
55  QString ret;
56 
57  // We have the client hostname
58  if (!d->remoteHostname.isEmpty()) {
59  ret = d->remoteHostname;
60  return ret;
61  }
62 
63  const QHostInfo ptr = QHostInfo::fromName(d->engineRequest->remoteAddress.toString());
64  if (ptr.error() != QHostInfo::NoError) {
65  qCDebug(CUTELYST_REQUEST) << "DNS lookup for the client hostname failed"
66  << d->engineRequest->remoteAddress;
67  return ret;
68  }
69 
70  d->remoteHostname = ptr.hostName();
71  ret = d->remoteHostname;
72  return ret;
73 }
74 
75 quint16 Request::port() const noexcept
76 {
77  Q_D(const Request);
78  return d->engineRequest->remotePort;
79 }
80 
81 QUrl Request::uri() const
82 {
83  Q_D(const Request);
84 
85  QUrl uri = d->url;
86  if (!(d->parserStatus & RequestPrivate::UrlParsed)) {
87  // This is a hack just in case remote is not set
88  if (d->engineRequest->serverAddress.isEmpty()) {
90  } else {
91  uri.setAuthority(QString::fromLatin1(d->engineRequest->serverAddress));
92  }
93 
94  uri.setScheme(d->engineRequest->isSecure ? QStringLiteral("https")
95  : QStringLiteral("http"));
96 
97  // if the path does not start with a slash it cleans the uri
98  // TODO check if engines will always set a slash
99  uri.setPath(d->engineRequest->path);
100 
101  if (!d->engineRequest->query.isEmpty()) {
102  uri.setQuery(QString::fromLatin1(d->engineRequest->query));
103  }
104 
105  d->url = uri;
106  d->parserStatus |= RequestPrivate::UrlParsed;
107  }
108  return uri;
109 }
110 
111 QString Request::base() const
112 {
113  Q_D(const Request);
114  QString base = d->base;
115  if (!(d->parserStatus & RequestPrivate::BaseParsed)) {
116  base = d->engineRequest->isSecure ? QStringLiteral("https://") : QStringLiteral("http://");
117 
118  // This is a hack just in case remote is not set
119  if (d->engineRequest->serverAddress.isEmpty()) {
121  } else {
122  base.append(QString::fromLatin1(d->engineRequest->serverAddress));
123  }
124 
125  d->base = base;
126  d->parserStatus |= RequestPrivate::BaseParsed;
127  }
128  return base;
129 }
130 
131 QString Request::path() const noexcept
132 {
133  Q_D(const Request);
134  return d->engineRequest->path;
135 }
136 
137 QString Request::match() const noexcept
138 {
139  Q_D(const Request);
140  return d->match;
141 }
142 
143 void Request::setMatch(const QString &match)
144 {
145  Q_D(Request);
146  d->match = match;
147 }
148 
149 QStringList Request::arguments() const noexcept
150 {
151  Q_D(const Request);
152  return d->args;
153 }
154 
155 void Request::setArguments(const QStringList &arguments)
156 {
157  Q_D(Request);
158  d->args = arguments;
159 }
160 
162 {
163  Q_D(const Request);
164  return d->captures;
165 }
166 
167 void Request::setCaptures(const QStringList &captures)
168 {
169  Q_D(Request);
170  d->captures = captures;
171 }
172 
173 bool Request::secure() const noexcept
174 {
175  Q_D(const Request);
176  return d->engineRequest->isSecure;
177 }
178 
179 QIODevice *Request::body() const noexcept
180 {
181  Q_D(const Request);
182  return d->body;
183 }
184 
186 {
187  Q_D(const Request);
188  if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
189  d->parseBody();
190  }
191  return d->bodyData;
192 }
193 
195 {
196  return bodyData().value<QCborValue>();
197 }
198 
200 {
201  return bodyData().toJsonDocument();
202 }
203 
205 {
206  return bodyData().toJsonDocument().object();
207 }
208 
210 {
211  return bodyData().toJsonDocument().array();
212 }
213 
215 {
216  return RequestPrivate::paramsMultiMapToVariantMap(bodyParameters());
217 }
218 
220 {
221  Q_D(const Request);
222  if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
223  d->parseBody();
224  }
225  return d->bodyParam;
226 }
227 
229 {
230  QStringList ret;
231 
232  const ParamsMultiMap query = bodyParameters();
233  auto it = query.constFind(key);
234  while (it != query.constEnd() && it.key() == key) {
235  ret.prepend(it.value());
236  ++it;
237  }
238  return ret;
239 }
240 
242 {
243  Q_D(const Request);
244  if (!(d->parserStatus & RequestPrivate::QueryParsed)) {
245  d->parseUrlQuery();
246  }
247  return d->queryKeywords;
248 }
249 
251 {
252  return RequestPrivate::paramsMultiMapToVariantMap(queryParameters());
253 }
254 
256 {
257  Q_D(const Request);
258  if (!(d->parserStatus & RequestPrivate::QueryParsed)) {
259  d->parseUrlQuery();
260  }
261  return d->queryParam;
262 }
263 
265 {
266  QStringList ret;
267 
268  const ParamsMultiMap query = queryParameters();
269  auto it = query.constFind(key);
270  while (it != query.constEnd() && it.key() == key) {
271  ret.prepend(it.value());
272  ++it;
273  }
274  return ret;
275 }
276 
278 {
279  Q_D(const Request);
280  if (!(d->parserStatus & RequestPrivate::CookiesParsed)) {
281  d->parseCookies();
282  }
283 
284  return d->cookies.value(name).value;
285 }
286 
288 {
289  QByteArrayList ret;
290  Q_D(const Request);
291 
292  if (!(d->parserStatus & RequestPrivate::CookiesParsed)) {
293  d->parseCookies();
294  }
295 
296  for (auto it = d->cookies.constFind(name); it != d->cookies.constEnd() && it->name == name;
297  ++it) {
298  ret.prepend(it->value);
299  }
300  return ret;
301 }
302 
304 {
305  Q_D(const Request);
306  if (!(d->parserStatus & RequestPrivate::CookiesParsed)) {
307  d->parseCookies();
308  }
309  return d->cookies;
310 }
311 
312 Headers Request::headers() const noexcept
313 {
314  Q_D(const Request);
315  return d->engineRequest->headers;
316 }
317 
318 QByteArray Request::method() const noexcept
319 {
320  Q_D(const Request);
321  return d->engineRequest->method;
322 }
323 
324 bool Request::isPost() const noexcept
325 {
326  Q_D(const Request);
327  return d->engineRequest->method.compare("POST") == 0;
328 }
329 
330 bool Request::isGet() const noexcept
331 {
332  Q_D(const Request);
333  return d->engineRequest->method.compare("GET") == 0;
334 }
335 
336 bool Request::isHead() const noexcept
337 {
338  Q_D(const Request);
339  return d->engineRequest->method.compare("HEAD") == 0;
340 }
341 
342 bool Request::isPut() const noexcept
343 {
344  Q_D(const Request);
345  return d->engineRequest->method.compare("PUT") == 0;
346 }
347 
348 bool Request::isPatch() const noexcept
349 {
350  Q_D(const Request);
351  return d->engineRequest->method.compare("PATCH") == 0;
352 }
353 
354 bool Request::isDelete() const noexcept
355 {
356  Q_D(const Request);
357  return d->engineRequest->method.compare("DELETE") == 0;
358 }
359 
360 QByteArray Request::protocol() const noexcept
361 {
362  Q_D(const Request);
363  return d->engineRequest->protocol;
364 }
365 
366 bool Request::xhr() const noexcept
367 {
368  Q_D(const Request);
369  return d->engineRequest->headers.header("X-Requested-With").compare("XMLHttpRequest") == 0;
370 }
371 
372 QString Request::remoteUser() const noexcept
373 {
374  Q_D(const Request);
375  return d->engineRequest->remoteUser;
376 }
377 
379 {
380  Q_D(const Request);
381  if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
382  d->parseBody();
383  }
384  return d->uploads;
385 }
386 
388 {
389  Q_D(const Request);
390  if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
391  d->parseBody();
392  }
393  return d->uploadsMap;
394 }
395 
397 {
398  Uploads ret;
399  const auto map = uploadsMap();
400  const auto range = map.equal_range(name);
401  for (auto i = range.first; i != range.second; ++i) {
402  ret.push_back(*i);
403  }
404  return ret;
405 }
406 
407 ParamsMultiMap Request::mangleParams(const ParamsMultiMap &args, bool append) const
408 {
409  ParamsMultiMap ret = queryParams();
410  if (append) {
411  ret.unite(args);
412  } else {
413  auto it = args.constEnd();
414  while (it != args.constBegin()) {
415  --it;
416  ret.replace(it.key(), it.value());
417  }
418  }
419 
420  return ret;
421 }
422 
423 QUrl Request::uriWith(const ParamsMultiMap &args, bool append) const
424 {
425  QUrl ret = uri();
426  QUrlQuery urlQuery;
427  const ParamsMultiMap query = mangleParams(args, append);
428  auto it = query.constEnd();
429  while (it != query.constBegin()) {
430  --it;
431  urlQuery.addQueryItem(it.key(), it.value());
432  }
433  ret.setQuery(urlQuery);
434 
435  return ret;
436 }
437 
438 Engine *Request::engine() const noexcept
439 {
440  Q_D(const Request);
441  return d->engine;
442 }
443 
444 void RequestPrivate::parseUrlQuery() const
445 {
446  // TODO move this to the asignment of query
447  if (engineRequest->query.size()) {
448  // Check for keywords (no = signs)
449  if (engineRequest->query.indexOf('=') < 0) {
450  QByteArray aux = engineRequest->query;
451  queryKeywords = Utils::decodePercentEncoding(&aux);
452  } else {
453  if (parserStatus & RequestPrivate::UrlParsed) {
454  queryParam = Utils::decodePercentEncoding(engineRequest->query.data(),
455  engineRequest->query.size());
456  } else {
457  QByteArray aux = engineRequest->query;
458  // We can't manipulate query directly
459  queryParam = Utils::decodePercentEncoding(aux.data(), aux.size());
460  }
461  }
462  }
463  parserStatus |= RequestPrivate::QueryParsed;
464 }
465 
466 void RequestPrivate::parseBody() const
467 {
468  if (!body) {
469  parserStatus |= RequestPrivate::BodyParsed;
470  return;
471  }
472 
473  bool sequencial = body->isSequential();
474  qint64 posOrig = body->pos();
475  if (sequencial && posOrig) {
476  qCWarning(CUTELYST_REQUEST) << "Can not parse sequential post body out of beginning";
477  parserStatus |= RequestPrivate::BodyParsed;
478  return;
479  }
480 
481  const QByteArray contentType = engineRequest->headers.header("Content-Type");
482  if (contentType.startsWith("application/x-www-form-urlencoded")) {
483  // Parse the query (BODY) of type "application/x-www-form-urlencoded"
484  // parameters ie "?foo=bar&bar=baz"
485  if (posOrig) {
486  body->seek(0);
487  }
488 
489  QByteArray line = body->readAll();
490  bodyParam = Utils::decodePercentEncoding(line.data(), line.size());
491  bodyData = QVariant::fromValue(bodyParam);
492  } else if (contentType.startsWith("multipart/form-data")) {
493  if (posOrig) {
494  body->seek(0);
495  }
496 
497  const Uploads ups = MultiPartFormDataParser::parse(body, contentType);
498  for (Upload *upload : ups) {
499  if (upload->filename().isEmpty() &&
500  upload->headers().header("Content-Type"_qba).isEmpty()) {
501  bodyParam.insert(upload->name(), QString::fromUtf8(upload->readAll()));
502  upload->seek(0);
503  }
504  uploadsMap.insert(upload->name(), upload);
505  }
506  uploads = ups;
507  // bodyData = QVariant::fromValue(uploadsMap);
508  } else if (contentType.startsWith("application/cbor")) {
509  if (posOrig) {
510  body->seek(0);
511  }
512 
513  bodyData = QVariant::fromValue(QCborValue::fromCbor(body->readAll()));
514  } else if (contentType.startsWith("application/json")) {
515  if (posOrig) {
516  body->seek(0);
517  }
518 
519  bodyData = QJsonDocument::fromJson(body->readAll());
520  }
521 
522  if (!sequencial) {
523  body->seek(posOrig);
524  }
525 
526  parserStatus |= RequestPrivate::BodyParsed;
527 }
528 
529 static inline bool isSlit(char c)
530 {
531  return c == ';' || c == ',';
532 }
533 
534 int findNextSplit(QByteArrayView text, int from, int length)
535 {
536  while (from < length) {
537  if (isSlit(text.at(from))) {
538  return from;
539  }
540  ++from;
541  }
542  return -1;
543 }
544 
545 static inline bool isLWS(char c)
546 {
547  return c == ' ' || c == '\t' || c == '\r' || c == '\n';
548 }
549 
550 static int nextNonWhitespace(QByteArrayView text, int from, int length)
551 {
552  // RFC 2616 defines linear whitespace as:
553  // LWS = [CRLF] 1*( SP | HT )
554  // We ignore the fact that CRLF must come as a pair at this point
555  // It's an invalid HTTP header if that happens.
556  while (from < length) {
557  if (isLWS(text.at(from)))
558  ++from;
559  else
560  return from; // non-whitespace
561  }
562 
563  // reached the end
564  return text.length();
565 }
566 
567 static Request::Cookie nextField(QByteArrayView text, int &position)
568 {
569  Request::Cookie cookie;
570  // format is one of:
571  // (1) token
572  // (2) token = token
573  // (3) token = quoted-string
574  const int length = text.length();
575  position = nextNonWhitespace(text, position, length);
576 
577  int semiColonPosition = findNextSplit(text, position, length);
578  if (semiColonPosition < 0)
579  semiColonPosition = length; // no ';' means take everything to end of string
580 
581  int equalsPosition = text.indexOf('=', position);
582  if (equalsPosition < 0 || equalsPosition > semiColonPosition) {
583  return cookie; //'=' is required for name-value-pair (RFC6265 section 5.2, rule 2)
584  }
585 
586  // TODO Qt 6.3
587  // ret.first = text.sliced(position, equalsPosition - position).trimmed().toByteArray();
588  cookie.name = text.sliced(position, equalsPosition - position).toByteArray().trimmed();
589  int secondLength = semiColonPosition - equalsPosition - 1;
590  if (secondLength > 0) {
591  // TODO Qt 6.3
592  // ret.second = text.sliced(equalsPosition + 1,
593  // secondLength).trimmed().toByteArray();
594  cookie.value = text.sliced(equalsPosition + 1, secondLength).toByteArray().trimmed();
595  }
596 
597  position = semiColonPosition;
598  return cookie;
599 }
600 
601 void RequestPrivate::parseCookies() const
602 {
603  const QByteArray cookieString = engineRequest->headers.header("Cookie"_qba);
604  int position = 0;
605  const int length = cookieString.length();
606  while (position < length) {
607  const auto cookie = nextField(cookieString, position);
608  if (cookie.name.isEmpty()) {
609  // parsing error
610  break;
611  }
612 
613  // Some foreign cookies are not in name=value format, so ignore them.
614  if (cookie.value.isEmpty()) {
615  ++position;
616  continue;
617  }
618  cookies.insert(cookie.name, cookie);
619  ++position;
620  }
621 
622  parserStatus |= RequestPrivate::CookiesParsed;
623 }
624 
625 QVariantMap RequestPrivate::paramsMultiMapToVariantMap(const ParamsMultiMap &params)
626 {
627  QVariantMap ret;
628  auto end = params.constEnd();
629  while (params.constBegin() != end) {
630  --end;
631  ret.insert(ret.constBegin(), end.key(), end.value());
632  }
633  return ret;
634 }
635 
636 #include "moc_request.cpp"
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QString url(QUrl::FormattingOptions options) const const
Request(EngineRequest *engineRequest)
Definition: request.cpp:19
QString & append(QChar ch)
QByteArray toByteArray() const const
QMultiMap::const_iterator constFind(const Key &key) const const
QJsonDocument toJsonDocument() const const
virtual ~Request()
Definition: request.cpp:26
QJsonArray array() const const
QByteArray trimmed() const const
QMultiMap::iterator replace(const Key &key, const T &value)
QVariant bodyData() const
void push_back(QList::parameter_type value)
static Uploads parse(QIODevice *body, QByteArrayView contentType, int bufferSize=4096)
Parser for multipart/formdata.
QByteArray protocol() const noexcept
QMultiMap::const_iterator constEnd() const const
QCborValue bodyCbor() const
Definition: request.cpp:194
bool isDelete() const noexcept
Definition: request.cpp:354
QJsonObject object() const const
QStringList arguments() const noexcept
bool isEmpty() const const
bool startsWith(QByteArrayView bv) const const
Container for HTTP headers.
Definition: headers.h:23
QJsonArray bodyJsonArray() const
Definition: request.cpp:209
QUrl uri() const
T value() const const
QByteArrayView sliced(qsizetype pos) const const
QString toString() const const
QString fromUtf8(QByteArrayView str)
qsizetype length() const const
void setMatch(const QString &match)
Definition: request.cpp:143
QIODevice * body() const noexcept
Definition: request.cpp:179
QString match() const noexcept
Cutelyst Upload handles file upload requests.
Definition: upload.h:25
void setPath(const QString &path, QUrl::ParsingMode mode)
QMultiMap::const_iterator constBegin() const const
ParamsMultiMap queryParams() const
ParamsMultiMap bodyParameters() const
Definition: request.cpp:219
QCborValue fromCbor(QCborStreamReader &reader)
void addQueryItem(const QString &key, const QString &value)
QVector< Upload * > uploads() const
Definition: request.cpp:378
QHostAddress address() const noexcept
Definition: request.cpp:33
void setAuthority(const QString &authority, QUrl::ParsingMode mode)
QString addressString() const
Definition: request.cpp:39
Headers headers() const noexcept
Definition: request.cpp:312
bool isGet() const noexcept
Definition: request.cpp:330
QString hostname() const
void setScheme(const QString &scheme)
QMultiMap< QByteArrayView, Cookie > cookies() const
Definition: request.cpp:303
The Cutelyst namespace holds all public Cutelyst API.
const Key & key() const const
QStringList args() const noexcept
QJsonObject bodyJsonObject() const
Definition: request.cpp:204
void setCaptures(const QStringList &captures)
Definition: request.cpp:167
qsizetype length() const const
void setArguments(const QStringList &arguments)
Definition: request.cpp:155
QJsonDocument bodyJsonDocument() const
Definition: request.cpp:199
QVariant fromValue(const T &value)
QHostInfo fromName(const QString &name)
ParamsMultiMap queryParameters() const
Definition: request.cpp:255
bool isPost() const noexcept
Definition: request.cpp:324
QByteArray method() const noexcept
ParamsMultiMap mangleParams(const ParamsMultiMap &args, bool append=false) const
Definition: request.cpp:407
bool xhr() const noexcept
Definition: request.cpp:366
QString fromLatin1(QByteArrayView str)
QMultiMap< Key, T > & unite(const QMultiMap< Key, T > &other)
qsizetype indexOf(QByteArrayView bv, qsizetype from) const const
quint16 port() const noexcept
QStringList captures() const noexcept
Definition: request.cpp:161
char * data()
void setQuery(const QString &query, QUrl::ParsingMode mode)
QString queryKeywords() const
Definition: request.cpp:241
QString localHostName()
char at(qsizetype n) const const
bool isPut() const noexcept
Definition: request.cpp:342
void prepend(QList::rvalue_ref value)
QVariantMap bodyParametersVariant() const
Definition: request.cpp:214
QByteArray cookie(QByteArrayView name) const
Definition: request.cpp:277
void setHost(const QString &host, QUrl::ParsingMode mode)
bool secure() const noexcept
bool isPatch() const noexcept
Definition: request.cpp:348
QString base() const
QUrl uriWith(const ParamsMultiMap &args, bool append=false) const
Definition: request.cpp:423
A request.
Definition: request.h:41
QList::const_iterator constEnd() const const
QList::const_iterator constBegin() const const
qsizetype size() const const
QHostInfo::HostInfoError error() const const
QVariantMap queryParametersVariant() const
Definition: request.cpp:250
QString remoteUser() const noexcept
The Cutelyst Engine.
Definition: engine.h:19
bool isHead() const noexcept
Definition: request.cpp:336
QString path() const noexcept
Engine * engine() const noexcept
Definition: request.cpp:438
QMultiMap< QStringView, Upload * > uploadsMap() const
Definition: request.cpp:387
QString hostName() const const