cutelyst  5.0.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-2024 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 ? u"https"_s : u"http"_s);
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 ? u"https://"_s : u"http://"_s;
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").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 namespace {
530 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 inline bool isLWS(char c)
547 {
548  return c == ' ' || c == '\t' || c == '\r' || c == '\n';
549 }
550 
551 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 
565  // reached the end
566  return text.length();
567 }
568 
569 Request::Cookie nextField(QByteArrayView text, int &position)
570 {
571  Request::Cookie cookie;
572  // format is one of:
573  // (1) token
574  // (2) token = token
575  // (3) token = quoted-string
576  const int length = text.length();
577  position = nextNonWhitespace(text, position, length);
578 
579  int semiColonPosition = findNextSplit(text, position, length);
580  if (semiColonPosition < 0) {
581  semiColonPosition = length; // no ';' means take everything to end of string
582  }
583 
584  int equalsPosition = text.indexOf('=', position);
585  if (equalsPosition < 0 || equalsPosition > semiColonPosition) {
586  return cookie; //'=' is required for name-value-pair (RFC6265 section 5.2, rule 2)
587  }
588 
589  cookie.name = text.sliced(position, equalsPosition - position).trimmed().toByteArray();
590  int secondLength = semiColonPosition - equalsPosition - 1;
591  if (secondLength > 0) {
592  cookie.value = text.sliced(equalsPosition + 1, secondLength).trimmed().toByteArray();
593  }
594 
595  position = semiColonPosition;
596  return cookie;
597 }
598 } // namespace
599 
600 void RequestPrivate::parseCookies() const
601 {
602  const QByteArray cookieString = engineRequest->headers.header("Cookie");
603  int position = 0;
604  const int length = cookieString.length();
605  while (position < length) {
606  const auto cookie = nextField(cookieString, position);
607  if (cookie.name.isEmpty()) {
608  // parsing error
609  break;
610  }
611 
612  // Some foreign cookies are not in name=value format, so ignore them.
613  if (cookie.value.isEmpty()) {
614  ++position;
615  continue;
616  }
617  cookies.insert(cookie.name, cookie);
618  ++position;
619  }
620 
621  parserStatus |= RequestPrivate::CookiesParsed;
622 }
623 
624 QVariantMap RequestPrivate::paramsMultiMapToVariantMap(const ParamsMultiMap &params)
625 {
626  QVariantMap ret;
627  auto end = params.constEnd();
628  while (params.constBegin() != end) {
629  --end;
630  ret.insert(ret.constBegin(), end.key(), end.value());
631  }
632  return ret;
633 }
634 
635 #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:194
bool isDelete() const noexcept
Definition: request.cpp:354
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: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, ParsingMode mode)
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:34
void setAuthority(const QString &authority, ParsingMode mode)
QMultiMap< QAnyStringView, Cookie > cookies() const
Definition: request.cpp:303
QString addressString() const
Definition: request.cpp:40
Headers headers() const noexcept
Definition: request.cpp:312
bool isGet() const noexcept
Definition: request.cpp:330
QString hostname() const
void setScheme(const QString &scheme)
The Cutelyst namespace holds all public Cutelyst API.
const Key & key() const const
QStringList args() const noexcept
QJsonObject bodyJsonObject() const
Definition: request.cpp:204
QByteArrayView trimmed() const const
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
void prepend(parameter_type 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)
qsizetype indexOf(QByteArrayView bv, qsizetype from) const const
quint16 port() const noexcept
QStringList captures() const noexcept
Definition: request.cpp:161
QByteArray cookie(QAnyStringView name) const
Definition: request.cpp:277
char * data()
void setQuery(const QString &query, 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
QVariantMap bodyParametersVariant() const
Definition: request.cpp:214
void setHost(const QString &host, 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
const_iterator constEnd() const const
const_iterator constBegin() const const
qsizetype size() const const
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
QMultiMap< QAnyStringView, Upload * > uploadsMap() const
Definition: request.cpp:387
QMultiMap< Key, T > & unite(QMultiMap< Key, T > &&other)
QString path() const noexcept
Engine * engine() const noexcept
Definition: request.cpp:438
QString hostName() const const