cutelyst 4.9.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
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 ? 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()) {
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
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
162QStringList Request::captures() const noexcept
163{
164 Q_D(const Request);
165 return d->captures;
166}
167
168void Request::setCaptures(const QStringList &captures)
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
195QCborValue Request::bodyCbor() const
196{
197 return bodyData().value<QCborValue>();
198}
199
200QJsonDocument Request::bodyJsonDocument() const
201{
202 return bodyData().toJsonDocument();
203}
204
205QJsonObject Request::bodyJsonObject() const
206{
207 return bodyData().toJsonDocument().object();
208}
209
210QJsonArray Request::bodyJsonArray() const
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
229QStringList Request::bodyParameters(const QString &key) const
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
265QStringList Request::queryParameters(const QString &key) const
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
278QByteArray Request::cookie(QByteArrayView name) const
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
288QByteArrayList Request::cookies(QByteArrayView name) const
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
304QMultiMap<QByteArrayView, Request::Cookie> Request::cookies() const
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
379QVector<Upload *> Request::uploads() const
380{
381 Q_D(const Request);
382 if (!(d->parserStatus & RequestPrivate::BodyParsed)) {
383 d->parseBody();
384 }
385 return d->uploads;
386}
387
388QMultiMap<QStringView, Cutelyst::Upload *> Request::uploadsMap() const
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{
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"_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
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
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
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
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
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
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.