cutelyst 5.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-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
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()) {
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
112QString Request::base() const
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()) {
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
150QStringList Request::arguments() const noexcept
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
186QVariant Request::bodyData() const
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
201{
202 return bodyData().toJsonDocument();
203}
204
206{
207 return bodyData().toJsonDocument().object();
208}
209
211{
212 return bodyData().toJsonDocument().array();
213}
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
319QByteArray Request::method() const noexcept
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
361QByteArray Request::protocol() const noexcept
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
373QString Request::remoteUser() const noexcept
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
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{
410 ParamsMultiMap ret = queryParams();
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").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
530namespace {
531inline bool isSlit(char c)
532{
533 return c == ';' || c == ',';
534}
535
536int findNextSplit(QByteArrayView text, int from, int length)
537{
538 while (from < length) {
539 if (isSlit(text.at(from))) {
540 return from;
541 }
542 ++from;
543 }
544 return -1;
545}
546
547inline bool isLWS(char c)
548{
549 return c == ' ' || c == '\t' || c == '\r' || c == '\n';
550}
551
552int nextNonWhitespace(QByteArrayView text, int from, int length)
553{
554 // RFC 2616 defines linear whitespace as:
555 // LWS = [CRLF] 1*( SP | HT )
556 // We ignore the fact that CRLF must come as a pair at this point
557 // It's an invalid HTTP header if that happens.
558 while (from < length) {
559 if (isLWS(text.at(from))) {
560 ++from;
561 } else {
562 return from; // non-whitespace
563 }
564 }
565
566 // reached the end
567 return text.length();
568}
569
570Request::Cookie nextField(QByteArrayView text, int &position)
571{
572 Request::Cookie cookie;
573 // format is one of:
574 // (1) token
575 // (2) token = token
576 // (3) token = quoted-string
577 const int length = text.length();
578 position = nextNonWhitespace(text, position, length);
579
580 int semiColonPosition = findNextSplit(text, position, length);
581 if (semiColonPosition < 0) {
582 semiColonPosition = length; // no ';' means take everything to end of string
583 }
584
585 int equalsPosition = text.indexOf('=', position);
586 if (equalsPosition < 0 || equalsPosition > semiColonPosition) {
587 return cookie; //'=' is required for name-value-pair (RFC6265 section 5.2, rule 2)
588 }
589
590 cookie.name = text.sliced(position, equalsPosition - position).trimmed().toByteArray();
591 int secondLength = semiColonPosition - equalsPosition - 1;
592 if (secondLength > 0) {
593 cookie.value = text.sliced(equalsPosition + 1, secondLength).trimmed().toByteArray();
594 }
595
596 position = semiColonPosition;
597 return cookie;
598}
599} // namespace
600
601void RequestPrivate::parseCookies() const
602{
603 const QByteArray cookieString = engineRequest->headers.header("Cookie");
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:215
QCborValue bodyCbor() const
Definition request.cpp:195
QVariantMap queryParametersVariant() const
Definition request.cpp:251
QMultiMap< QAnyStringView, Cookie > cookies() const
Definition request.cpp:304
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
virtual ~Request()
Definition request.cpp:27
QJsonArray bodyJsonArray() const
Definition request.cpp:210
bool xhr() const noexcept
Definition request.cpp:367
QJsonObject bodyJsonObject() const
Definition request.cpp:205
QStringList captures() const noexcept
Definition request.cpp:162
bool isPut() const noexcept
Definition request.cpp:343
bool isDelete() const noexcept
Definition request.cpp:355
QMultiMap< QAnyStringView, Upload * > uploadsMap() const
Definition request.cpp:388
QUrl uriWith(const ParamsMultiMap &args, bool append=false) const
Definition request.cpp:424
bool isPost() const noexcept
Definition request.cpp:325
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
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
QByteArray cookie(QAnyStringView name) const
Definition request.cpp:278
Request(EngineRequest *engineRequest)
Definition request.cpp:20
bool isHead() const noexcept
Definition request.cpp:337
void setArguments(const QStringList &arguments)
Definition request.cpp:156
QHostAddress address() const noexcept
Definition request.cpp:34
void setMatch(const QString &match)
Definition request.cpp:144
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
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::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(T &&value)
QJsonDocument toJsonDocument() const const
T value() const const