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
17using namespace Cutelyst;
18using 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
34QHostAddress Request::address() const noexcept
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
53QString Request::hostname() const
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
76quint16 Request::port() const noexcept
77{
78 Q_D(const Request);
79 return d->engineRequest->remotePort;
80}
81
82QUrl 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()) {
90 uri.setHost(QHostInfo::localHostName());
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
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 ? 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()) {
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(QAnyStringView 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(QAnyStringView 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<QAnyStringView, 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<QAnyStringView, 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(QAnyStringView 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").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
529namespace {
530inline bool isSlit(char c)
531{
532 return c == ';' || c == ',';
533}
534
535int 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
546inline bool isLWS(char c)
547{
548 return c == ' ' || c == '\t' || c == '\r' || c == '\n';
549}
550
551int 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
569Request::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
600void 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
624QVariantMap 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"
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.
QVariantMap bodyParametersVariant() const
Definition request.cpp:214
QCborValue bodyCbor() const
Definition request.cpp:194
QVariantMap queryParametersVariant() const
Definition request.cpp:250
QMultiMap< QAnyStringView, Cookie > cookies() const
Definition request.cpp:303
QString addressString() const
Definition request.cpp:40
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:27
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
QMultiMap< QAnyStringView, Upload * > uploadsMap() const
Definition request.cpp:387
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
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
QByteArray cookie(QAnyStringView name) const
Definition request.cpp:277
Request(EngineRequest *engineRequest)
Definition request.cpp:20
bool isHead() const noexcept
Definition request.cpp:336
void setArguments(const QStringList &arguments)
Definition request.cpp:155
QHostAddress address() const noexcept
Definition request.cpp:34
void setMatch(const QString &match)
Definition request.cpp:143
Cutelyst Upload handles file upload requests.
Definition upload.h:26
QMultiMap< QString, QString > ParamsMultiMap
The Cutelyst namespace holds all public Cutelyst API.