cutelyst 4.3.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
17using 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
52QString 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
75quint16 Request::port() const noexcept
76{
77 Q_D(const Request);
78 return d->engineRequest->remotePort;
79}
80
81QUrl 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
111QString 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
131QString Request::path() const noexcept
132{
133 Q_D(const Request);
134 return d->engineRequest->path;
135}
136
137QString Request::match() const noexcept
138{
139 Q_D(const Request);
140 return d->match;
141}
142
143void Request::setMatch(const QString &match)
144{
145 Q_D(Request);
146 d->match = match;
147}
148
149QStringList Request::arguments() const noexcept
150{
151 Q_D(const Request);
152 return d->args;
153}
154
155void 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
168{
169 Q_D(Request);
170 d->captures = captures;
171}
172
173bool Request::secure() const noexcept
174{
175 Q_D(const Request);
176 return d->engineRequest->isSecure;
177}
178
179QIODevice *Request::body() const noexcept
180{
181 Q_D(const Request);
182 return d->body;
183}
184
185QVariant 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
312Headers Request::headers() const noexcept
313{
314 Q_D(const Request);
315 return d->engineRequest->headers;
316}
317
318QByteArray Request::method() const noexcept
319{
320 Q_D(const Request);
321 return d->engineRequest->method;
322}
323
324bool Request::isPost() const noexcept
325{
326 Q_D(const Request);
327 return d->engineRequest->method.compare("POST") == 0;
328}
329
330bool Request::isGet() const noexcept
331{
332 Q_D(const Request);
333 return d->engineRequest->method.compare("GET") == 0;
334}
335
336bool Request::isHead() const noexcept
337{
338 Q_D(const Request);
339 return d->engineRequest->method.compare("HEAD") == 0;
340}
341
342bool Request::isPut() const noexcept
343{
344 Q_D(const Request);
345 return d->engineRequest->method.compare("PUT") == 0;
346}
347
348bool Request::isPatch() const noexcept
349{
350 Q_D(const Request);
351 return d->engineRequest->method.compare("PATCH") == 0;
352}
353
354bool Request::isDelete() const noexcept
355{
356 Q_D(const Request);
357 return d->engineRequest->method.compare("DELETE") == 0;
358}
359
360QByteArray Request::protocol() const noexcept
361{
362 Q_D(const Request);
363 return d->engineRequest->protocol;
364}
365
366bool Request::xhr() const noexcept
367{
368 Q_D(const Request);
369 return d->engineRequest->headers.header("X-Requested-With").compare("XMLHttpRequest") == 0;
370}
371
372QString 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
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
423QUrl 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
438Engine *Request::engine() const noexcept
439{
440 Q_D(const Request);
441 return d->engine;
442}
443
444void 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
466void 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
529static inline bool isSlit(char c)
530{
531 return c == ';' || c == ',';
532}
533
534int 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
545static inline bool isLWS(char c)
546{
547 return c == ' ' || c == '\t' || c == '\r' || c == '\n';
548}
549
550static 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
567static 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
601void 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
625QVariantMap 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(const T &value)
QJsonDocument toJsonDocument() const const
T value() const const