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