cutelyst 4.8.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;
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
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
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
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 ? QStringLiteral("https")
96 : QStringLiteral("http"));
97
98 // if the path does not start with a slash it cleans the uri
99 // TODO check if engines will always set a slash
100 uri.setPath(d->engineRequest->path);
101
102 if (!d->engineRequest->query.isEmpty()) {
103 uri.setQuery(QString::fromLatin1(d->engineRequest->query));
104 }
105
106 d->url = uri;
107 d->parserStatus |= RequestPrivate::UrlParsed;
108 }
109 return uri;
110}
111
113{
114 Q_D(const Request);
115 QString base = d->base;
116 if (!(d->parserStatus & RequestPrivate::BaseParsed)) {
117 base = d->engineRequest->isSecure ? QStringLiteral("https://") : QStringLiteral("http://");
118
119 // This is a hack just in case remote is not set
120 if (d->engineRequest->serverAddress.isEmpty()) {
121 base.append(QHostInfo::localHostName());
122 } else {
123 base.append(QString::fromLatin1(d->engineRequest->serverAddress));
124 }
125
126 d->base = base;
127 d->parserStatus |= RequestPrivate::BaseParsed;
128 }
129 return base;
130}
131
132QString Request::path() const noexcept
133{
134 Q_D(const Request);
135 return d->engineRequest->path;
136}
137
138QString Request::match() const noexcept
139{
140 Q_D(const Request);
141 return d->match;
142}
143
144void Request::setMatch(const QString &match)
145{
146 Q_D(Request);
147 d->match = match;
148}
149
151{
152 Q_D(const Request);
153 return d->args;
154}
155
156void Request::setArguments(const QStringList &arguments)
157{
158 Q_D(Request);
159 d->args = arguments;
160}
161
163{
164 Q_D(const Request);
165 return d->captures;
166}
167
169{
170 Q_D(Request);
171 d->captures = captures;
172}
173
174bool Request::secure() const noexcept
175{
176 Q_D(const Request);
177 return d->engineRequest->isSecure;
178}
179
180QIODevice *Request::body() const noexcept
181{
182 Q_D(const Request);
183 return d->body;
184}
185
187{
188 Q_D(const Request);
189 if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
190 d->parseBody();
191 }
192 return d->bodyData;
193}
194
196{
197 return bodyData().value<QCborValue>();
198}
199
204
209
214
216{
217 return RequestPrivate::paramsMultiMapToVariantMap(bodyParameters());
218}
219
221{
222 Q_D(const Request);
223 if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
224 d->parseBody();
225 }
226 return d->bodyParam;
227}
228
230{
231 QStringList ret;
232
233 const ParamsMultiMap query = bodyParameters();
234 auto it = query.constFind(key);
235 while (it != query.constEnd() && it.key() == key) {
236 ret.prepend(it.value());
237 ++it;
238 }
239 return ret;
240}
241
243{
244 Q_D(const Request);
245 if (!(d->parserStatus & RequestPrivate::QueryParsed)) {
246 d->parseUrlQuery();
247 }
248 return d->queryKeywords;
249}
250
252{
253 return RequestPrivate::paramsMultiMapToVariantMap(queryParameters());
254}
255
257{
258 Q_D(const Request);
259 if (!(d->parserStatus & RequestPrivate::QueryParsed)) {
260 d->parseUrlQuery();
261 }
262 return d->queryParam;
263}
264
266{
267 QStringList ret;
268
269 const ParamsMultiMap query = queryParameters();
270 auto it = query.constFind(key);
271 while (it != query.constEnd() && it.key() == key) {
272 ret.prepend(it.value());
273 ++it;
274 }
275 return ret;
276}
277
279{
280 Q_D(const Request);
281 if (!(d->parserStatus & RequestPrivate::CookiesParsed)) {
282 d->parseCookies();
283 }
284
285 return d->cookies.value(name).value;
286}
287
289{
290 QByteArrayList ret;
291 Q_D(const Request);
292
293 if (!(d->parserStatus & RequestPrivate::CookiesParsed)) {
294 d->parseCookies();
295 }
296
297 for (auto it = d->cookies.constFind(name); it != d->cookies.constEnd() && it->name == name;
298 ++it) {
299 ret.prepend(it->value);
300 }
301 return ret;
302}
303
305{
306 Q_D(const Request);
307 if (!(d->parserStatus & RequestPrivate::CookiesParsed)) {
308 d->parseCookies();
309 }
310 return d->cookies;
311}
312
313Headers Request::headers() const noexcept
314{
315 Q_D(const Request);
316 return d->engineRequest->headers;
317}
318
320{
321 Q_D(const Request);
322 return d->engineRequest->method;
323}
324
325bool Request::isPost() const noexcept
326{
327 Q_D(const Request);
328 return d->engineRequest->method.compare("POST") == 0;
329}
330
331bool Request::isGet() const noexcept
332{
333 Q_D(const Request);
334 return d->engineRequest->method.compare("GET") == 0;
335}
336
337bool Request::isHead() const noexcept
338{
339 Q_D(const Request);
340 return d->engineRequest->method.compare("HEAD") == 0;
341}
342
343bool Request::isPut() const noexcept
344{
345 Q_D(const Request);
346 return d->engineRequest->method.compare("PUT") == 0;
347}
348
349bool Request::isPatch() const noexcept
350{
351 Q_D(const Request);
352 return d->engineRequest->method.compare("PATCH") == 0;
353}
354
355bool Request::isDelete() const noexcept
356{
357 Q_D(const Request);
358 return d->engineRequest->method.compare("DELETE") == 0;
359}
360
362{
363 Q_D(const Request);
364 return d->engineRequest->protocol;
365}
366
367bool Request::xhr() const noexcept
368{
369 Q_D(const Request);
370 return d->engineRequest->headers.header("X-Requested-With").compare("XMLHttpRequest") == 0;
371}
372
374{
375 Q_D(const Request);
376 return d->engineRequest->remoteUser;
377}
378
380{
381 Q_D(const Request);
382 if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
383 d->parseBody();
384 }
385 return d->uploads;
386}
387
389{
390 Q_D(const Request);
391 if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
392 d->parseBody();
393 }
394 return d->uploadsMap;
395}
396
397Uploads Request::uploads(QStringView name) const
398{
399 Uploads ret;
400 const auto map = uploadsMap();
401 const auto range = map.equal_range(name);
402 for (auto i = range.first; i != range.second; ++i) {
403 ret.push_back(*i);
404 }
405 return ret;
406}
407
409{
411 if (append) {
412 ret.unite(args);
413 } else {
414 auto it = args.constEnd();
415 while (it != args.constBegin()) {
416 --it;
417 ret.replace(it.key(), it.value());
418 }
419 }
420
421 return ret;
422}
423
424QUrl Request::uriWith(const ParamsMultiMap &args, bool append) const
425{
426 QUrl ret = uri();
427 QUrlQuery urlQuery;
428 const ParamsMultiMap query = mangleParams(args, append);
429 auto it = query.constEnd();
430 while (it != query.constBegin()) {
431 --it;
432 urlQuery.addQueryItem(it.key(), it.value());
433 }
434 ret.setQuery(urlQuery);
435
436 return ret;
437}
438
439Engine *Request::engine() const noexcept
440{
441 Q_D(const Request);
442 return d->engine;
443}
444
445void RequestPrivate::parseUrlQuery() const
446{
447 // TODO move this to the asignment of query
448 if (engineRequest->query.size()) {
449 // Check for keywords (no = signs)
450 if (engineRequest->query.indexOf('=') < 0) {
451 QByteArray aux = engineRequest->query;
452 queryKeywords = Utils::decodePercentEncoding(&aux);
453 } else {
454 if (parserStatus & RequestPrivate::UrlParsed) {
455 queryParam = Utils::decodePercentEncoding(engineRequest->query.data(),
456 engineRequest->query.size());
457 } else {
458 QByteArray aux = engineRequest->query;
459 // We can't manipulate query directly
460 queryParam = Utils::decodePercentEncoding(aux.data(), aux.size());
461 }
462 }
463 }
464 parserStatus |= RequestPrivate::QueryParsed;
465}
466
467void RequestPrivate::parseBody() const
468{
469 if (!body) {
470 parserStatus |= RequestPrivate::BodyParsed;
471 return;
472 }
473
474 bool sequencial = body->isSequential();
475 qint64 posOrig = body->pos();
476 if (sequencial && posOrig) {
477 qCWarning(CUTELYST_REQUEST) << "Can not parse sequential post body out of beginning";
478 parserStatus |= RequestPrivate::BodyParsed;
479 return;
480 }
481
482 const QByteArray contentType = engineRequest->headers.header("Content-Type");
483 if (contentType.startsWith("application/x-www-form-urlencoded")) {
484 // Parse the query (BODY) of type "application/x-www-form-urlencoded"
485 // parameters ie "?foo=bar&bar=baz"
486 if (posOrig) {
487 body->seek(0);
488 }
489
490 QByteArray line = body->readAll();
491 bodyParam = Utils::decodePercentEncoding(line.data(), line.size());
492 bodyData = QVariant::fromValue(bodyParam);
493 } else if (contentType.startsWith("multipart/form-data")) {
494 if (posOrig) {
495 body->seek(0);
496 }
497
498 const Uploads ups = MultiPartFormDataParser::parse(body, contentType);
499 for (Upload *upload : ups) {
500 if (upload->filename().isEmpty() &&
501 upload->headers().header("Content-Type"_ba).isEmpty()) {
502 bodyParam.insert(upload->name(), QString::fromUtf8(upload->readAll()));
503 upload->seek(0);
504 }
505 uploadsMap.insert(upload->name(), upload);
506 }
507 uploads = ups;
508 // bodyData = QVariant::fromValue(uploadsMap);
509 } else if (contentType.startsWith("application/cbor")) {
510 if (posOrig) {
511 body->seek(0);
512 }
513
514 bodyData = QVariant::fromValue(QCborValue::fromCbor(body->readAll()));
515 } else if (contentType.startsWith("application/json")) {
516 if (posOrig) {
517 body->seek(0);
518 }
519
520 bodyData = QJsonDocument::fromJson(body->readAll());
521 }
522
523 if (!sequencial) {
524 body->seek(posOrig);
525 }
526
527 parserStatus |= RequestPrivate::BodyParsed;
528}
529
530static inline 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
546static inline bool isLWS(char c)
547{
548 return c == ' ' || c == '\t' || c == '\r' || c == '\n';
549}
550
551static int 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 // reached the end
565 return text.length();
566}
567
568static Request::Cookie nextField(QByteArrayView text, int &position)
569{
570 Request::Cookie cookie;
571 // format is one of:
572 // (1) token
573 // (2) token = token
574 // (3) token = quoted-string
575 const int length = text.length();
576 position = nextNonWhitespace(text, position, length);
577
578 int semiColonPosition = findNextSplit(text, position, length);
579 if (semiColonPosition < 0)
580 semiColonPosition = length; // no ';' means take everything to end of string
581
582 int equalsPosition = text.indexOf('=', position);
583 if (equalsPosition < 0 || equalsPosition > semiColonPosition) {
584 return cookie; //'=' is required for name-value-pair (RFC6265 section 5.2, rule 2)
585 }
586
587 cookie.name = text.sliced(position, equalsPosition - position).trimmed().toByteArray();
588 int secondLength = semiColonPosition - equalsPosition - 1;
589 if (secondLength > 0) {
590 cookie.value = text.sliced(equalsPosition + 1, secondLength).trimmed().toByteArray();
591 }
592
593 position = semiColonPosition;
594 return cookie;
595}
596
597void RequestPrivate::parseCookies() const
598{
599 const QByteArray cookieString = engineRequest->headers.header("Cookie"_ba);
600 int position = 0;
601 const int length = cookieString.length();
602 while (position < length) {
603 const auto cookie = nextField(cookieString, position);
604 if (cookie.name.isEmpty()) {
605 // parsing error
606 break;
607 }
608
609 // Some foreign cookies are not in name=value format, so ignore them.
610 if (cookie.value.isEmpty()) {
611 ++position;
612 continue;
613 }
614 cookies.insert(cookie.name, cookie);
615 ++position;
616 }
617
618 parserStatus |= RequestPrivate::CookiesParsed;
619}
620
621QVariantMap RequestPrivate::paramsMultiMapToVariantMap(const ParamsMultiMap &params)
622{
623 QVariantMap ret;
624 auto end = params.constEnd();
625 while (params.constBegin() != end) {
626 --end;
627 ret.insert(ret.constBegin(), end.key(), end.value());
628 }
629 return ret;
630}
631
632#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:215
QCborValue bodyCbor() const
Definition request.cpp:195
QVariantMap queryParametersVariant() const
Definition request.cpp:251
QString addressString() const
Definition request.cpp:40
bool isGet() const noexcept
Definition request.cpp:331
QString queryKeywords() const
Definition request.cpp:242
QVector< Upload * > uploads() const
Definition request.cpp:379
ParamsMultiMap bodyParameters() const
Definition request.cpp:220
QVariant bodyData() const
Definition request.cpp:186
quint16 port() const noexcept
Definition request.cpp:76
virtual ~Request()
Definition request.cpp:27
QJsonArray bodyJsonArray() const
Definition request.cpp:210
bool xhr() const noexcept
Definition request.cpp:367
bool secure() const noexcept
Definition request.cpp:174
QJsonObject bodyJsonObject() const
Definition request.cpp:205
QString path() const noexcept
Definition request.cpp:132
ParamsMultiMap queryParams() const
Definition request.h:586
QStringList captures() const noexcept
Definition request.cpp:162
bool isPut() const noexcept
Definition request.cpp:343
bool isDelete() const noexcept
Definition request.cpp:355
QByteArray protocol() const noexcept
Definition request.cpp:361
QByteArray cookie(QByteArrayView name) const
Definition request.cpp:278
QUrl uriWith(const ParamsMultiMap &args, bool append=false) const
Definition request.cpp:424
bool isPost() const noexcept
Definition request.cpp:325
QUrl uri() const
Definition request.cpp:82
QString remoteUser() const noexcept
Definition request.cpp:373
QString base() const
Definition request.cpp:112
QJsonDocument bodyJsonDocument() const
Definition request.cpp:200
ParamsMultiMap mangleParams(const ParamsMultiMap &args, bool append=false) const
Definition request.cpp:408
void setCaptures(const QStringList &captures)
Definition request.cpp:168
QMultiMap< QByteArrayView, Cookie > cookies() const
Definition request.cpp:304
QString hostname() const
Definition request.cpp:53
Headers headers() const noexcept
Definition request.cpp:313
QIODevice * body() const noexcept
Definition request.cpp:180
ParamsMultiMap queryParameters() const
Definition request.cpp:256
bool isPatch() const noexcept
Definition request.cpp:349
Engine * engine() const noexcept
Definition request.cpp:439
Request(EngineRequest *engineRequest)
Definition request.cpp:20
QByteArray method() const noexcept
Definition request.cpp:319
bool isHead() const noexcept
Definition request.cpp:337
void setArguments(const QStringList &arguments)
Definition request.cpp:156
QString match() const noexcept
Definition request.cpp:138
QStringList arguments() const noexcept
Definition request.cpp:150
QHostAddress address() const noexcept
Definition request.cpp:34
void setMatch(const QString &match)
Definition request.cpp:144
QMultiMap< QStringView, Upload * > uploadsMap() const
Definition request.cpp:388
Cutelyst Upload handles file upload requests.
Definition upload.h:26
QMultiMap< QString, QString > ParamsMultiMap
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
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
QByteArrayView trimmed() 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< T >::parameter_type value)
QMultiMap< Key, T >::const_iterator constBegin() const const
QMultiMap< Key, T >::const_iterator constEnd() const const
QMultiMap< Key, T >::const_iterator constFind(const Key &key) const const
QMultiMap< Key, T >::iterator replace(const Key &key, const T &value)
QMultiMap< Key, T > & unite(QMultiMap< Key, T > &&other)
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
void setQuery(const QString &query, QUrl::ParsingMode mode)
QString url(QUrl::FormattingOptions options) const const
void addQueryItem(const QString &key, const QString &value)
QVariant fromValue(T &&value)
QJsonDocument toJsonDocument() const const
T value() const &const