cutelyst 4.0.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
26Request::~Request()
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
194QJsonDocument Request::bodyJsonDocument() const
195{
196 return bodyData().toJsonDocument();
197}
198
199QJsonObject Request::bodyJsonObject() const
200{
201 return bodyData().toJsonDocument().object();
202}
203
204QJsonArray Request::bodyJsonArray() const
205{
206 return bodyData().toJsonDocument().array();
207}
208
210{
211 return RequestPrivate::paramsMultiMapToVariantMap(bodyParameters());
212}
213
215{
216 Q_D(const Request);
217 if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
218 d->parseBody();
219 }
220 return d->bodyParam;
221}
222
223QStringList Request::bodyParameters(const QString &key) const
224{
225 QStringList ret;
226
227 const ParamsMultiMap query = bodyParameters();
228 auto it = query.constFind(key);
229 while (it != query.constEnd() && it.key() == key) {
230 ret.prepend(it.value());
231 ++it;
232 }
233 return ret;
234}
235
237{
238 Q_D(const Request);
239 if (!(d->parserStatus & RequestPrivate::QueryParsed)) {
240 d->parseUrlQuery();
241 }
242 return d->queryKeywords;
243}
244
246{
247 return RequestPrivate::paramsMultiMapToVariantMap(queryParameters());
248}
249
251{
252 Q_D(const Request);
253 if (!(d->parserStatus & RequestPrivate::QueryParsed)) {
254 d->parseUrlQuery();
255 }
256 return d->queryParam;
257}
258
259QStringList Request::queryParameters(const QString &key) const
260{
261 QStringList ret;
262
263 const ParamsMultiMap query = queryParameters();
264 auto it = query.constFind(key);
265 while (it != query.constEnd() && it.key() == key) {
266 ret.prepend(it.value());
267 ++it;
268 }
269 return ret;
270}
271
272QByteArray Request::cookie(QByteArrayView name) const
273{
274 Q_D(const Request);
275 if (!(d->parserStatus & RequestPrivate::CookiesParsed)) {
276 d->parseCookies();
277 }
278
279 return d->cookies.value(name).value;
280}
281
282QByteArrayList Request::cookies(QByteArrayView name) const
283{
284 QByteArrayList ret;
285 Q_D(const Request);
286
287 if (!(d->parserStatus & RequestPrivate::CookiesParsed)) {
288 d->parseCookies();
289 }
290
291 for (auto it = d->cookies.constFind(name); it != d->cookies.constEnd() && it->name == name;
292 ++it) {
293 ret.prepend(it->value);
294 }
295 return ret;
296}
297
298QMultiMap<QByteArrayView, Request::Cookie> Request::cookies() const
299{
300 Q_D(const Request);
301 if (!(d->parserStatus & RequestPrivate::CookiesParsed)) {
302 d->parseCookies();
303 }
304 return d->cookies;
305}
306
307Headers Request::headers() const noexcept
308{
309 Q_D(const Request);
310 return d->engineRequest->headers;
311}
312
313QByteArray Request::method() const noexcept
314{
315 Q_D(const Request);
316 return d->engineRequest->method;
317}
318
319bool Request::isPost() const noexcept
320{
321 Q_D(const Request);
322 return d->engineRequest->method.compare("POST") == 0;
323}
324
325bool Request::isGet() const noexcept
326{
327 Q_D(const Request);
328 return d->engineRequest->method.compare("GET") == 0;
329}
330
331bool Request::isHead() const noexcept
332{
333 Q_D(const Request);
334 return d->engineRequest->method.compare("HEAD") == 0;
335}
336
337bool Request::isPut() const noexcept
338{
339 Q_D(const Request);
340 return d->engineRequest->method.compare("PUT") == 0;
341}
342
343bool Request::isPatch() const noexcept
344{
345 Q_D(const Request);
346 return d->engineRequest->method.compare("PATCH") == 0;
347}
348
349bool Request::isDelete() const noexcept
350{
351 Q_D(const Request);
352 return d->engineRequest->method.compare("DELETE") == 0;
353}
354
355QByteArray Request::protocol() const noexcept
356{
357 Q_D(const Request);
358 return d->engineRequest->protocol;
359}
360
361bool Request::xhr() const noexcept
362{
363 Q_D(const Request);
364 return d->engineRequest->headers.header("X-Requested-With").compare("XMLHttpRequest") == 0;
365}
366
367QString Request::remoteUser() const noexcept
368{
369 Q_D(const Request);
370 return d->engineRequest->remoteUser;
371}
372
373QVector<Upload *> Request::uploads() const
374{
375 Q_D(const Request);
376 if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
377 d->parseBody();
378 }
379 return d->uploads;
380}
381
382QMultiMap<QStringView, Cutelyst::Upload *> Request::uploadsMap() const
383{
384 Q_D(const Request);
385 if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
386 d->parseBody();
387 }
388 return d->uploadsMap;
389}
390
391Uploads Request::uploads(QStringView name) const
392{
393 Uploads ret;
394 const auto map = uploadsMap();
395 const auto range = map.equal_range(name);
396 for (auto i = range.first; i != range.second; ++i) {
397 ret.push_back(*i);
398 }
399 return ret;
400}
401
403{
404 ParamsMultiMap ret = queryParams();
405 if (append) {
406 ret.unite(args);
407 } else {
408 auto it = args.constEnd();
409 while (it != args.constBegin()) {
410 --it;
411 ret.replace(it.key(), it.value());
412 }
413 }
414
415 return ret;
416}
417
418QUrl Request::uriWith(const ParamsMultiMap &args, bool append) const
419{
420 QUrl ret = uri();
421 QUrlQuery urlQuery;
422 const ParamsMultiMap query = mangleParams(args, append);
423 auto it = query.constEnd();
424 while (it != query.constBegin()) {
425 --it;
426 urlQuery.addQueryItem(it.key(), it.value());
427 }
428 ret.setQuery(urlQuery);
429
430 return ret;
431}
432
433Engine *Request::engine() const noexcept
434{
435 Q_D(const Request);
436 return d->engine;
437}
438
439void RequestPrivate::parseUrlQuery() const
440{
441 // TODO move this to the asignment of query
442 if (engineRequest->query.size()) {
443 // Check for keywords (no = signs)
444 if (engineRequest->query.indexOf('=') < 0) {
445 QByteArray aux = engineRequest->query;
446 queryKeywords = Utils::decodePercentEncoding(&aux);
447 } else {
448 if (parserStatus & RequestPrivate::UrlParsed) {
449 queryParam = Utils::decodePercentEncoding(engineRequest->query.data(),
450 engineRequest->query.size());
451 } else {
452 QByteArray aux = engineRequest->query;
453 // We can't manipulate query directly
454 queryParam = Utils::decodePercentEncoding(aux.data(), aux.size());
455 }
456 }
457 }
458 parserStatus |= RequestPrivate::QueryParsed;
459}
460
461void RequestPrivate::parseBody() const
462{
463 if (!body) {
464 parserStatus |= RequestPrivate::BodyParsed;
465 return;
466 }
467
468 bool sequencial = body->isSequential();
469 qint64 posOrig = body->pos();
470 if (sequencial && posOrig) {
471 qCWarning(CUTELYST_REQUEST) << "Can not parse sequential post body out of beginning";
472 parserStatus |= RequestPrivate::BodyParsed;
473 return;
474 }
475
476 const QByteArray contentType = engineRequest->headers.header("Content-Type"_qba);
477 if (contentType.startsWith("application/x-www-form-urlencoded")) {
478 // Parse the query (BODY) of type "application/x-www-form-urlencoded"
479 // parameters ie "?foo=bar&bar=baz"
480 if (posOrig) {
481 body->seek(0);
482 }
483
484 QByteArray line = body->readAll();
485 bodyParam = Utils::decodePercentEncoding(line.data(), line.size());
486 bodyData = QVariant::fromValue(bodyParam);
487 } else if (contentType.startsWith("multipart/form-data")) {
488 if (posOrig) {
489 body->seek(0);
490 }
491
492 const Uploads ups = MultiPartFormDataParser::parse(body, contentType);
493 for (Upload *upload : ups) {
494 if (upload->filename().isEmpty() &&
495 upload->headers().header("Content-Type"_qba).isEmpty()) {
496 bodyParam.insert(upload->name(), QString::fromUtf8(upload->readAll()));
497 upload->seek(0);
498 }
499 uploadsMap.insert(upload->name(), upload);
500 }
501 uploads = ups;
502 // bodyData = QVariant::fromValue(uploadsMap);
503 } else if (contentType.startsWith("application/json")) {
504 if (posOrig) {
505 body->seek(0);
506 }
507
508 bodyData = QJsonDocument::fromJson(body->readAll());
509 }
510
511 if (!sequencial) {
512 body->seek(posOrig);
513 }
514
515 parserStatus |= RequestPrivate::BodyParsed;
516}
517
518static inline bool isSlit(char c)
519{
520 return c == ';' || c == ',';
521}
522
523int findNextSplit(QByteArrayView text, int from, int length)
524{
525 while (from < length) {
526 if (isSlit(text.at(from))) {
527 return from;
528 }
529 ++from;
530 }
531 return -1;
532}
533
534static inline bool isLWS(char c)
535{
536 return c == ' ' || c == '\t' || c == '\r' || c == '\n';
537}
538
539static int nextNonWhitespace(QByteArrayView text, int from, int length)
540{
541 // RFC 2616 defines linear whitespace as:
542 // LWS = [CRLF] 1*( SP | HT )
543 // We ignore the fact that CRLF must come as a pair at this point
544 // It's an invalid HTTP header if that happens.
545 while (from < length) {
546 if (isLWS(text.at(from)))
547 ++from;
548 else
549 return from; // non-whitespace
550 }
551
552 // reached the end
553 return text.length();
554}
555
556static Request::Cookie nextField(QByteArrayView text, int &position)
557{
558 Request::Cookie cookie;
559 // format is one of:
560 // (1) token
561 // (2) token = token
562 // (3) token = quoted-string
563 const int length = text.length();
564 position = nextNonWhitespace(text, position, length);
565
566 int semiColonPosition = findNextSplit(text, position, length);
567 if (semiColonPosition < 0)
568 semiColonPosition = length; // no ';' means take everything to end of string
569
570 int equalsPosition = text.indexOf('=', position);
571 if (equalsPosition < 0 || equalsPosition > semiColonPosition) {
572 return cookie; //'=' is required for name-value-pair (RFC6265 section 5.2, rule 2)
573 }
574
575 // TODO Qt 6.3
576 // ret.first = text.sliced(position, equalsPosition - position).trimmed().toByteArray();
577 cookie.name = text.sliced(position, equalsPosition - position).toByteArray().trimmed();
578 int secondLength = semiColonPosition - equalsPosition - 1;
579 if (secondLength > 0) {
580 // TODO Qt 6.3
581 // ret.second = text.sliced(equalsPosition + 1,
582 // secondLength).trimmed().toByteArray();
583 cookie.value = text.sliced(equalsPosition + 1, secondLength).toByteArray().trimmed();
584 }
585
586 position = semiColonPosition;
587 return cookie;
588}
589
590void RequestPrivate::parseCookies() const
591{
592 const QByteArray cookieString = engineRequest->headers.header("Cookie"_qba);
593 int position = 0;
594 const int length = cookieString.length();
595 while (position < length) {
596 const auto cookie = nextField(cookieString, position);
597 if (cookie.name.isEmpty()) {
598 // parsing error
599 break;
600 }
601
602 // Some foreign cookies are not in name=value format, so ignore them.
603 if (cookie.value.isEmpty()) {
604 ++position;
605 continue;
606 }
607 cookies.insert(cookie.name, cookie);
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:20
static Uploads parse(QIODevice *body, QByteArrayView contentType, int bufferSize=4096)
Parser for multipart/formdata.
QVariantMap bodyParametersVariant() const
Definition: request.cpp:209
QVariantMap queryParametersVariant() const
Definition: request.cpp:245
QString addressString() const
Definition: request.cpp:39
bool isGet() const noexcept
Definition: request.cpp:325
QString queryKeywords() const
Definition: request.cpp:236
QVector< Upload * > uploads() const
Definition: request.cpp:373
ParamsMultiMap bodyParameters() const
Definition: request.cpp:214
QJsonArray bodyJsonArray() const
Definition: request.cpp:204
bool xhr() const noexcept
Definition: request.cpp:361
QJsonObject bodyJsonObject() const
Definition: request.cpp:199
QStringList captures() const noexcept
Definition: request.cpp:161
bool isPut() const noexcept
Definition: request.cpp:337
bool isDelete() const noexcept
Definition: request.cpp:349
QByteArray cookie(QByteArrayView name) const
Definition: request.cpp:272
QUrl uriWith(const ParamsMultiMap &args, bool append=false) const
Definition: request.cpp:418
bool isPost() const noexcept
Definition: request.cpp:319
QJsonDocument bodyJsonDocument() const
Definition: request.cpp:194
ParamsMultiMap mangleParams(const ParamsMultiMap &args, bool append=false) const
Definition: request.cpp:402
void setCaptures(const QStringList &captures)
Definition: request.cpp:167
QMultiMap< QByteArrayView, Cookie > cookies() const
Definition: request.cpp:298
Headers headers() const noexcept
Definition: request.cpp:307
QIODevice * body() const noexcept
Definition: request.cpp:179
ParamsMultiMap queryParameters() const
Definition: request.cpp:250
bool isPatch() const noexcept
Definition: request.cpp:343
Engine * engine() const noexcept
Definition: request.cpp:433
Request(EngineRequest *engineRequest)
Definition: request.cpp:19
bool isHead() const noexcept
Definition: request.cpp:331
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:382
Cutelyst Upload handles file upload request
Definition: upload.h:22
The Cutelyst namespace holds all public Cutelyst API.
Definition: Mainpage.dox:8
QVector< Upload * > Uploads
Definition: request.h:26
QMultiMap< QString, QString > ParamsMultiMap