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
17using namespace Cutelyst;
18
20 : d_ptr(new RequestPrivate)
21{
22 d_ptr->engineRequest = engineRequest;
23 d_ptr->body = engineRequest->body;
24}
25
26Request::~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
75quint16 Request::port() const
76{
77 Q_D(const Request);
78 return d->engineRequest->remotePort;
79}
80
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()) {
89 uri.setHost(QHostInfo::localHostName());
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
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()) {
119 base.append(QHostInfo::localHostName());
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
133QString Request::path() const noexcept
134{
135 Q_D(const Request);
136 return d->engineRequest->path;
137}
138
139QString Request::match() const noexcept
140{
141 Q_D(const Request);
142 return d->match;
143}
144
145void Request::setMatch(const QString &match)
146{
147 Q_D(Request);
148 d->match = match;
149}
150
152{
153 Q_D(const Request);
154 return d->args;
155}
156
157void 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
170{
171 Q_D(Request);
172 d->captures = captures;
173}
174
175bool 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
200
205
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
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
310Headers Request::headers() const noexcept
311{
312 Q_D(const Request);
313 return d->engineRequest->headers;
314}
315
316QString Request::method() const noexcept
317{
318 Q_D(const Request);
319 return d->engineRequest->method;
320}
321
322bool Request::isPost() const noexcept
323{
324 Q_D(const Request);
325 return d->engineRequest->method.compare(u"POST") == 0;
326}
327
328bool Request::isGet() const noexcept
329{
330 Q_D(const Request);
331 return d->engineRequest->method.compare(u"GET") == 0;
332}
333
334bool Request::isHead() const noexcept
335{
336 Q_D(const Request);
337 return d->engineRequest->method.compare(u"HEAD") == 0;
338}
339
340bool Request::isPut() const noexcept
341{
342 Q_D(const Request);
343 return d->engineRequest->method.compare(u"PUT") == 0;
344}
345
346bool Request::isPatch() const noexcept
347{
348 Q_D(const Request);
349 return d->engineRequest->method.compare(u"PATCH") == 0;
350}
351
352bool Request::isDelete() const noexcept
353{
354 Q_D(const Request);
355 return d->engineRequest->method.compare(u"DELETE") == 0;
356}
357
359{
360 Q_D(const Request);
361 return d->engineRequest->protocol;
362}
363
364bool 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
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
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
407{
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
422QUrl 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
437Engine *Request::engine() const noexcept
438{
439 Q_D(const Request);
440 return d->engine;
441}
442
443void 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
465void 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
523static inline bool isSlit(QChar c)
524{
525 return c == u';' || c == u',';
526}
527
528int 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
539static inline bool isLWS(QChar c)
540{
541 return c == u' ' || c == u'\t' || c == u'\r' || c == u'\n';
542}
543
544static 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
561static 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
590void 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
614QVariantMap 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"
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:386
QVariantMap bodyParametersVariant() const
Definition request.cpp:211
QString method() const noexcept
Definition request.cpp:316
QVariantMap queryParametersVariant() const
Definition request.cpp:247
QString addressString() const
Definition request.cpp:39
bool isGet() const noexcept
Definition request.cpp:328
QString queryKeywords() const
Definition request.cpp:238
QVector< Upload * > uploads() const
Definition request.cpp:377
ParamsMultiMap bodyParameters() const
Definition request.cpp:216
QVariant bodyData() const
Definition request.cpp:187
QJsonArray bodyJsonArray() const
Definition request.cpp:206
bool xhr() const noexcept
Definition request.cpp:364
bool secure() const noexcept
Definition request.cpp:175
QJsonObject bodyJsonObject() const
Definition request.cpp:201
QString path() const noexcept
Definition request.cpp:133
ParamsMultiMap queryParams() const
Definition request.h:556
QStringList captures() const noexcept
Definition request.cpp:163
bool isPut() const noexcept
Definition request.cpp:340
bool isDelete() const noexcept
Definition request.cpp:352
QUrl uriWith(const ParamsMultiMap &args, bool append=false) const
Definition request.cpp:422
bool isPost() const noexcept
Definition request.cpp:322
QUrl uri() const
Definition request.cpp:81
QString remoteUser() const noexcept
Definition request.cpp:371
QString base() const
Definition request.cpp:110
quint16 port() const
Definition request.cpp:75
QString protocol() const noexcept
Definition request.cpp:358
QJsonDocument bodyJsonDocument() const
Definition request.cpp:196
ParamsMultiMap mangleParams(const ParamsMultiMap &args, bool append=false) const
Definition request.cpp:406
void setCaptures(const QStringList &captures)
Definition request.cpp:169
QString hostname() const
Definition request.cpp:52
Headers headers() const noexcept
Definition request.cpp:310
ParamsMultiMap queryParameters() const
Definition request.cpp:252
QIODevice * body() const
Definition request.cpp:181
QString cookie(const QString &name) const
Definition request.cpp:274
bool isPatch() const noexcept
Definition request.cpp:346
Engine * engine() const noexcept
Definition request.cpp:437
Request(EngineRequest *engineRequest)
Definition request.cpp:19
bool isHead() const noexcept
Definition request.cpp:334
void setArguments(const QStringList &arguments)
Definition request.cpp:157
QString match() const noexcept
Definition request.cpp:139
QStringList arguments() const noexcept
Definition request.cpp:151
QHostAddress address() const noexcept
Definition request.cpp:33
void setMatch(const QString &match)
Definition request.cpp:145
ParamsMultiMap cookies() const
Definition request.cpp:301
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:25
QMultiMap< QString, QString > ParamsMultiMap
char * data()
int size() const const
QString toString() const const
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(const T &value)
typename QMap< Key, T >::const_iterator constFind(const Key &key, const T &value) const const
typename QMap< Key, T >::iterator replace(const Key &key, const T &value)
QMultiMap< Key, T > & unite(const QMultiMap< Key, T > &other)
const QChar at(int position) const const
QString fromLatin1(const char *str, int size)
QString fromUtf8(const char *str, int size)
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
int length() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
CaseInsensitive
void setQuery(const QString &query, ParsingMode mode)
QString url(FormattingOptions options) const const
void addQueryItem(const QString &key, const QString &value)
QVariant fromValue(const T &value)
QJsonDocument toJsonDocument() const const
void push_back(const T &value)