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