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
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
33QHostAddress Request::address() const noexcept
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()) {
89 uri.setHost(QHostInfo::localHostName());
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()) {
120 base.append(QHostInfo::localHostName());
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
161QStringList Request::captures() const noexcept
162{
163 Q_D(const Request);
164 return d->captures;
165}
166
167void Request::setCaptures(const QStringList &captures)
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
194QCborValue Request::bodyCbor() const
195{
196 return bodyData().value<QCborValue>();
197}
198
199QJsonDocument Request::bodyJsonDocument() const
200{
201 return bodyData().toJsonDocument();
202}
203
204QJsonObject Request::bodyJsonObject() const
205{
206 return bodyData().toJsonDocument().object();
207}
208
209QJsonArray Request::bodyJsonArray() const
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
228QStringList Request::bodyParameters(const QString &key) const
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
264QStringList Request::queryParameters(const QString &key) const
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
277QByteArray Request::cookie(QByteArrayView name) const
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
287QByteArrayList Request::cookies(QByteArrayView name) const
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
303QMultiMap<QByteArrayView, Request::Cookie> Request::cookies() const
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
378QVector<Upload *> Request::uploads() const
379{
380 Q_D(const Request);
381 if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
382 d->parseBody();
383 }
384 return d->uploads;
385}
386
387QMultiMap<QStringView, Cutelyst::Upload *> Request::uploadsMap() const
388{
389 Q_D(const Request);
390 if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
391 d->parseBody();
392 }
393 return d->uploadsMap;
394}
395
396Uploads Request::uploads(QStringView name) const
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
QMultiMap< QString, QString > ParamsMultiMap
The Cutelyst namespace holds all public Cutelyst API.