cutelyst  3.9.1
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 
26 Request::~Request()
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
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(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  uri.setPath(QLatin1Char('/') + d->engineRequest->path);
99 
100  if (!d->engineRequest->query.isEmpty()) {
101  uri.setQuery(QString::fromLatin1(d->engineRequest->query));
102  }
103 
104  d->url = uri;
105  d->parserStatus |= RequestPrivate::UrlParsed;
106  }
107  return uri;
108 }
109 
110 QString Request::base() const
111 {
112  Q_D(const Request);
113  QString base = d->base;
114  if (!(d->parserStatus & RequestPrivate::BaseParsed)) {
115  base = d->engineRequest->isSecure ? QStringLiteral("https://") : QStringLiteral("http://");
116 
117  // This is a hack just in case remote is not set
118  if (d->engineRequest->serverAddress.isEmpty()) {
120  } else {
121  base.append(d->engineRequest->serverAddress);
122  }
123 
124  // base always have a trailing slash
125  base.append(QLatin1Char('/'));
126 
127  d->base = base;
128  d->parserStatus |= RequestPrivate::BaseParsed;
129  }
130  return base;
131 }
132 
133 QString Request::path() const noexcept
134 {
135  Q_D(const Request);
136  return d->engineRequest->path;
137 }
138 
139 QString Request::match() const noexcept
140 {
141  Q_D(const Request);
142  return d->match;
143 }
144 
145 void Request::setMatch(const QString &match)
146 {
147  Q_D(Request);
148  d->match = match;
149 }
150 
151 QStringList Request::arguments() const noexcept
152 {
153  Q_D(const Request);
154  return d->args;
155 }
156 
157 void Request::setArguments(const QStringList &arguments)
158 {
159  Q_D(Request);
160  d->args = arguments;
161 }
162 
164 {
165  Q_D(const Request);
166  return d->captures;
167 }
168 
169 void Request::setCaptures(const QStringList &captures)
170 {
171  Q_D(Request);
172  d->captures = captures;
173 }
174 
175 bool Request::secure() const noexcept
176 {
177  Q_D(const Request);
178  return d->engineRequest->isSecure;
179 }
180 
182 {
183  Q_D(const Request);
184  return d->body;
185 }
186 
188 {
189  Q_D(const Request);
190  if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
191  d->parseBody();
192  }
193  return d->bodyData;
194 }
195 
197 {
198  return bodyData().toJsonDocument();
199 }
200 
202 {
203  return bodyData().toJsonDocument().object();
204 }
205 
207 {
208  return bodyData().toJsonDocument().array();
209 }
210 
212 {
213  return RequestPrivate::paramsMultiMapToVariantMap(bodyParameters());
214 }
215 
217 {
218  Q_D(const Request);
219  if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
220  d->parseBody();
221  }
222  return d->bodyParam;
223 }
224 
226 {
227  QStringList ret;
228 
229  const ParamsMultiMap query = bodyParameters();
230  auto it = query.constFind(key);
231  while (it != query.constEnd() && it.key() == key) {
232  ret.prepend(it.value());
233  ++it;
234  }
235  return ret;
236 }
237 
239 {
240  Q_D(const Request);
241  if (!(d->parserStatus & RequestPrivate::QueryParsed)) {
242  d->parseUrlQuery();
243  }
244  return d->queryKeywords;
245 }
246 
248 {
249  return RequestPrivate::paramsMultiMapToVariantMap(queryParameters());
250 }
251 
253 {
254  Q_D(const Request);
255  if (!(d->parserStatus & RequestPrivate::QueryParsed)) {
256  d->parseUrlQuery();
257  }
258  return d->queryParam;
259 }
260 
262 {
263  QStringList ret;
264 
265  const ParamsMultiMap query = queryParameters();
266  auto it = query.constFind(key);
267  while (it != query.constEnd() && it.key() == key) {
268  ret.prepend(it.value());
269  ++it;
270  }
271  return ret;
272 }
273 
274 QString Request::cookie(const QString &name) const
275 {
276  Q_D(const Request);
277  if (!(d->parserStatus & RequestPrivate::CookiesParsed)) {
278  d->parseCookies();
279  }
280 
281  return d->cookies.value(name);
282 }
283 
285 {
286  QStringList ret;
287  Q_D(const Request);
288 
289  if (!(d->parserStatus & RequestPrivate::CookiesParsed)) {
290  d->parseCookies();
291  }
292 
293  auto it = d->cookies.constFind(name);
294  while (it != d->cookies.constEnd() && it.key() == name) {
295  ret.prepend(it.value());
296  ++it;
297  }
298  return ret;
299 }
300 
302 {
303  Q_D(const Request);
304  if (!(d->parserStatus & RequestPrivate::CookiesParsed)) {
305  d->parseCookies();
306  }
307  return d->cookies;
308 }
309 
310 Headers Request::headers() const noexcept
311 {
312  Q_D(const Request);
313  return d->engineRequest->headers;
314 }
315 
316 QString Request::method() const noexcept
317 {
318  Q_D(const Request);
319  return d->engineRequest->method;
320 }
321 
322 bool Request::isPost() const noexcept
323 {
324  Q_D(const Request);
325  return d->engineRequest->method.compare(u"POST") == 0;
326 }
327 
328 bool Request::isGet() const noexcept
329 {
330  Q_D(const Request);
331  return d->engineRequest->method.compare(u"GET") == 0;
332 }
333 
334 bool Request::isHead() const noexcept
335 {
336  Q_D(const Request);
337  return d->engineRequest->method.compare(u"HEAD") == 0;
338 }
339 
340 bool Request::isPut() const noexcept
341 {
342  Q_D(const Request);
343  return d->engineRequest->method.compare(u"PUT") == 0;
344 }
345 
346 bool Request::isPatch() const noexcept
347 {
348  Q_D(const Request);
349  return d->engineRequest->method.compare(u"PATCH") == 0;
350 }
351 
352 bool Request::isDelete() const noexcept
353 {
354  Q_D(const Request);
355  return d->engineRequest->method.compare(u"DELETE") == 0;
356 }
357 
358 QString Request::protocol() const noexcept
359 {
360  Q_D(const Request);
361  return d->engineRequest->protocol;
362 }
363 
364 bool Request::xhr() const noexcept
365 {
366  Q_D(const Request);
367  return d->engineRequest->headers.header(QStringLiteral("X_REQUESTED_WITH"))
368  .compare(u"XMLHttpRequest") == 0;
369 }
370 
371 QString Request::remoteUser() const noexcept
372 {
373  Q_D(const Request);
374  return d->engineRequest->remoteUser;
375 }
376 
378 {
379  Q_D(const Request);
380  if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
381  d->parseBody();
382  }
383  return d->uploads;
384 }
385 
387 {
388  Q_D(const Request);
389  if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
390  d->parseBody();
391  }
392  return d->uploadsMap;
393 }
394 
395 Uploads Request::uploads(const QString &name) const
396 {
397  Uploads ret;
398  const auto map = uploadsMap();
399  const auto range = map.equal_range(name);
400  for (auto i = range.first; i != range.second; ++i) {
401  ret.push_back(*i);
402  }
403  return ret;
404 }
405 
406 ParamsMultiMap Request::mangleParams(const ParamsMultiMap &args, bool append) const
407 {
408  ParamsMultiMap ret = queryParams();
409  if (append) {
410  ret.unite(args);
411  } else {
412  auto it = args.constEnd();
413  while (it != args.constBegin()) {
414  --it;
415  ret.replace(it.key(), it.value());
416  }
417  }
418 
419  return ret;
420 }
421 
422 QUrl Request::uriWith(const ParamsMultiMap &args, bool append) const
423 {
424  QUrl ret = uri();
425  QUrlQuery urlQuery;
426  const ParamsMultiMap query = mangleParams(args, append);
427  auto it = query.constEnd();
428  while (it != query.constBegin()) {
429  --it;
430  urlQuery.addQueryItem(it.key(), it.value());
431  }
432  ret.setQuery(urlQuery);
433 
434  return ret;
435 }
436 
437 Engine *Request::engine() const noexcept
438 {
439  Q_D(const Request);
440  return d->engine;
441 }
442 
443 void RequestPrivate::parseUrlQuery() const
444 {
445  // TODO move this to the asignment of query
446  if (engineRequest->query.size()) {
447  // Check for keywords (no = signs)
448  if (engineRequest->query.indexOf('=') < 0) {
449  QByteArray aux = engineRequest->query;
450  queryKeywords = Utils::decodePercentEncoding(&aux);
451  } else {
452  if (parserStatus & RequestPrivate::UrlParsed) {
453  queryParam = Utils::decodePercentEncoding(engineRequest->query.data(),
454  engineRequest->query.size());
455  } else {
456  QByteArray aux = engineRequest->query;
457  // We can't manipulate query directly
458  queryParam = Utils::decodePercentEncoding(aux.data(), aux.size());
459  }
460  }
461  }
462  parserStatus |= RequestPrivate::QueryParsed;
463 }
464 
465 void RequestPrivate::parseBody() const
466 {
467  if (!body) {
468  parserStatus |= RequestPrivate::BodyParsed;
469  return;
470  }
471 
472  bool sequencial = body->isSequential();
473  qint64 posOrig = body->pos();
474  if (sequencial && posOrig) {
475  qCWarning(CUTELYST_REQUEST) << "Can not parse sequential post body out of beginning";
476  parserStatus |= RequestPrivate::BodyParsed;
477  return;
478  }
479 
480  const QString contentTypeKey = QStringLiteral("CONTENT_TYPE");
481  const QString contentType = engineRequest->headers.header(contentTypeKey);
482  if (contentType.startsWith(u"application/x-www-form-urlencoded", Qt::CaseInsensitive)) {
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(u"multipart/form-data", Qt::CaseInsensitive)) {
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(contentTypeKey).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(u"application/json", Qt::CaseInsensitive)) {
509  if (posOrig) {
510  body->seek(0);
511  }
512 
513  bodyData = QJsonDocument::fromJson(body->readAll());
514  }
515 
516  if (!sequencial) {
517  body->seek(posOrig);
518  }
519 
520  parserStatus |= RequestPrivate::BodyParsed;
521 }
522 
523 static inline bool isSlit(QChar c)
524 {
525  return c == u';' || c == u',';
526 }
527 
528 int findNextSplit(const QString &text, int from, int length)
529 {
530  while (from < length) {
531  if (isSlit(text.at(from))) {
532  return from;
533  }
534  ++from;
535  }
536  return -1;
537 }
538 
539 static inline bool isLWS(QChar c)
540 {
541  return c == u' ' || c == u'\t' || c == u'\r' || c == u'\n';
542 }
543 
544 static int nextNonWhitespace(const QString &text, int from, int length)
545 {
546  // RFC 2616 defines linear whitespace as:
547  // LWS = [CRLF] 1*( SP | HT )
548  // We ignore the fact that CRLF must come as a pair at this point
549  // It's an invalid HTTP header if that happens.
550  while (from < length) {
551  if (isLWS(text.at(from)))
552  ++from;
553  else
554  return from; // non-whitespace
555  }
556 
557  // reached the end
558  return text.length();
559 }
560 
561 static std::pair<QString, QString> nextField(const QString &text, int &position)
562 {
563  std::pair<QString, QString> ret;
564  // format is one of:
565  // (1) token
566  // (2) token = token
567  // (3) token = quoted-string
568  const int length = text.length();
569  position = nextNonWhitespace(text, position, length);
570 
571  int semiColonPosition = findNextSplit(text, position, length);
572  if (semiColonPosition < 0)
573  semiColonPosition = length; // no ';' means take everything to end of string
574 
575  int equalsPosition = text.indexOf(QLatin1Char('='), position);
576  if (equalsPosition < 0 || equalsPosition > semiColonPosition) {
577  return ret; //'=' is required for name-value-pair (RFC6265 section 5.2, rule 2)
578  }
579 
580  ret.first = QStringView(text).mid(position, equalsPosition - position).trimmed().toString();
581  int secondLength = semiColonPosition - equalsPosition - 1;
582  if (secondLength > 0) {
583  ret.second = QStringView(text).mid(equalsPosition + 1, secondLength).trimmed().toString();
584  }
585 
586  position = semiColonPosition;
587  return ret;
588 }
589 
590 void RequestPrivate::parseCookies() const
591 {
592  const QString cookieString = engineRequest->headers.header(QStringLiteral("COOKIE"));
593  int position = 0;
594  const int length = cookieString.length();
595  while (position < length) {
596  const auto field = nextField(cookieString, position);
597  if (field.first.isEmpty()) {
598  // parsing error
599  break;
600  }
601 
602  // Some foreign cookies are not in name=value format, so ignore them.
603  if (field.second.isEmpty()) {
604  ++position;
605  continue;
606  }
607  cookies.insert(field.first, field.second);
608  ++position;
609  }
610 
611  parserStatus |= RequestPrivate::CookiesParsed;
612 }
613 
614 QVariantMap RequestPrivate::paramsMultiMapToVariantMap(const ParamsMultiMap &params)
615 {
616  QVariantMap ret;
617  auto end = params.constEnd();
618  while (params.constBegin() != end) {
619  --end;
620  ret.insert(ret.constBegin(), end.key(), end.value());
621  }
622  return ret;
623 }
624 
625 #include "moc_request.cpp"
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QString url(FormattingOptions options) const const
static Uploads parse(QIODevice *body, QStringView contentType, int bufferSize=4096)
Parser for multipart/formdata.
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
Request(EngineRequest *engineRequest)
Definition: request.cpp:19
QString & append(QChar ch)
QIODevice * body() const
Definition: request.cpp:181
QString method() const noexcept
const_iterator constFind(const Key &key) const const
QJsonDocument toJsonDocument() const const
QJsonArray array() const const
iterator replace(const Key &key, const T &value)
QVariant bodyData() const
void push_back(parameter_type value)
const_iterator constEnd() const const
bool isDelete() const noexcept
Definition: request.cpp:352
QStringList args() const
QVariant fromValue(T &&value)
QJsonObject object() const const
QStringList arguments() const noexcept
QStringView mid(qsizetype start, qsizetype length) const const
QJsonArray bodyJsonArray() const
Definition: request.cpp:206
QUrl uri() const
QIODevice * body
The QIODevice containing the body (if any) of the request.
QString toString() const const
QString fromUtf8(QByteArrayView str)
void setMatch(const QString &match)
Definition: request.cpp:145
quint16 port() const
QString match() const noexcept
QString protocol() const noexcept
Cutelyst Upload handles file upload request
Definition: upload.h:22
void setPath(const QString &path, ParsingMode mode)
const_iterator constBegin() const const
ParamsMultiMap queryParams() const
ParamsMultiMap bodyParameters() const
Definition: request.cpp:216
void addQueryItem(const QString &key, const QString &value)
QVector< Upload * > uploads() const
Definition: request.cpp:377
QHostAddress address() const noexcept
Definition: request.cpp:33
void setAuthority(const QString &authority, ParsingMode mode)
QString addressString() const
Definition: request.cpp:39
QMultiMap< QString, Upload * > uploadsMap() const
Definition: request.cpp:386
Headers headers() const noexcept
Definition: request.cpp:310
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
CaseInsensitive
bool isGet() const noexcept
Definition: request.cpp:328
QString hostname() const
ParamsMultiMap cookies() const
Definition: request.cpp:301
void setScheme(const QString &scheme)
The Cutelyst namespace holds all public Cutelyst API.
Definition: Mainpage.dox:7
const Key & key() const const
QJsonObject bodyJsonObject() const
Definition: request.cpp:201
void setCaptures(const QStringList &captures)
Definition: request.cpp:169
QStringView trimmed() const const
void setArguments(const QStringList &arguments)
Definition: request.cpp:157
QJsonDocument bodyJsonDocument() const
Definition: request.cpp:196
void prepend(parameter_type value)
QHostInfo fromName(const QString &name)
ParamsMultiMap queryParameters() const
Definition: request.cpp:252
bool isPost() const noexcept
Definition: request.cpp:322
ParamsMultiMap mangleParams(const ParamsMultiMap &args, bool append=false) const
Definition: request.cpp:406
bool xhr() const noexcept
Definition: request.cpp:364
QString fromLatin1(QByteArrayView str)
QStringList captures() const noexcept
Definition: request.cpp:163
QString toString() const const
const QChar at(qsizetype position) const const
qsizetype length() const const
char * data()
void setQuery(const QString &query, ParsingMode mode)
QString queryKeywords() const
Definition: request.cpp:238
QString localHostName()
QString cookie(const QString &name) const
Definition: request.cpp:274
bool isPut() const noexcept
Definition: request.cpp:340
QVariantMap bodyParametersVariant() const
Definition: request.cpp:211
void setHost(const QString &host, ParsingMode mode)
bool secure() const noexcept
bool isPatch() const noexcept
Definition: request.cpp:346
QString base() const
QUrl uriWith(const ParamsMultiMap &args, bool append=false) const
Definition: request.cpp:422
const_iterator constEnd() const const
const_iterator constBegin() const const
qsizetype size() const const
HostInfoError error() const const
QVariantMap queryParametersVariant() const
Definition: request.cpp:247
QString remoteUser() const noexcept
The Cutelyst Engine
Definition: engine.h:20
bool isHead() const noexcept
Definition: request.cpp:334
QMultiMap< Key, T > & unite(QMultiMap< Key, T > &&other)
QString path() const noexcept
Engine * engine() const noexcept
Definition: request.cpp:437
QString hostName() const const