cutelyst  4.4.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 
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"
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 
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 
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"
The Cutelyst Engine.
Definition: engine.h:20
Container for HTTP headers.
Definition: headers.h:24
static Uploads parse(QIODevice *body, QByteArrayView contentType, int bufferSize=4096)
Parser for multipart/formdata.
A request.
Definition: request.h:42
QVariantMap bodyParametersVariant() const
Definition: request.cpp:214
QCborValue bodyCbor() const
Definition: request.cpp:194
QVariantMap queryParametersVariant() const
Definition: request.cpp:250
QString addressString() const
Definition: request.cpp:39
bool isGet() const noexcept
Definition: request.cpp:330
QString queryKeywords() const
Definition: request.cpp:241
QVector< Upload * > uploads() const
Definition: request.cpp:378
ParamsMultiMap bodyParameters() const
Definition: request.cpp:219
virtual ~Request()
Definition: request.cpp:26
QJsonArray bodyJsonArray() const
Definition: request.cpp:209
bool xhr() const noexcept
Definition: request.cpp:366
QJsonObject bodyJsonObject() const
Definition: request.cpp:204
QStringList captures() const noexcept
Definition: request.cpp:161
bool isPut() const noexcept
Definition: request.cpp:342
bool isDelete() const noexcept
Definition: request.cpp:354
QByteArray cookie(QByteArrayView name) const
Definition: request.cpp:277
QUrl uriWith(const ParamsMultiMap &args, bool append=false) const
Definition: request.cpp:423
bool isPost() const noexcept
Definition: request.cpp:324
QJsonDocument bodyJsonDocument() const
Definition: request.cpp:199
ParamsMultiMap mangleParams(const ParamsMultiMap &args, bool append=false) const
Definition: request.cpp:407
void setCaptures(const QStringList &captures)
Definition: request.cpp:167
QMultiMap< QByteArrayView, Cookie > cookies() const
Definition: request.cpp:303
Headers headers() const noexcept
Definition: request.cpp:312
QIODevice * body() const noexcept
Definition: request.cpp:179
ParamsMultiMap queryParameters() const
Definition: request.cpp:255
bool isPatch() const noexcept
Definition: request.cpp:348
Engine * engine() const noexcept
Definition: request.cpp:438
Request(EngineRequest *engineRequest)
Definition: request.cpp:19
bool isHead() const noexcept
Definition: request.cpp:336
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
QMultiMap< QStringView, Upload * > uploadsMap() const
Definition: request.cpp:387
Cutelyst Upload handles file upload requests.
Definition: upload.h:26
The Cutelyst namespace holds all public Cutelyst API.
char * data()
bool isEmpty() const const
qsizetype length() const const
qsizetype size() const const
bool startsWith(QByteArrayView bv) const const
QByteArray trimmed() const const
char at(qsizetype n) const const
qsizetype indexOf(QByteArrayView bv, qsizetype from) const const
qsizetype length() const const
QByteArrayView sliced(qsizetype pos) const const
QByteArray toByteArray() const const
QCborValue fromCbor(QCborStreamReader &reader)
QString toString() const const
QHostInfo::HostInfoError error() const const
QHostInfo fromName(const QString &name)
QString hostName() const const
QString localHostName()
QJsonArray array() const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
void prepend(QList::parameter_type value)
void push_back(QList::parameter_type value)
const Key & key() const const
QMultiMap::const_iterator constBegin() const const
QMultiMap::const_iterator constEnd() const const
QMultiMap::const_iterator constFind(const Key &key) const const
QMultiMap::iterator replace(const Key &key, const T &value)
QMultiMap< Key, T > & unite(QMultiMap< Key, T > &&other)
QString & append(QChar ch)
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
void setAuthority(const QString &authority, QUrl::ParsingMode mode)
void setHost(const QString &host, QUrl::ParsingMode mode)
void setPath(const QString &path, QUrl::ParsingMode mode)
void setQuery(const QString &query, QUrl::ParsingMode mode)
void setScheme(const QString &scheme)
QString url(QUrl::FormattingOptions options) const const
void addQueryItem(const QString &key, const QString &value)
QVariant fromValue(T &&value)
QJsonDocument toJsonDocument() const const
T value() const const