1// Copyright (C) 2023 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
5#include "qrestreply.h"
6#include "qrestreply_p.h"
7
8#include <QtNetwork/private/qnetworkreply_p.h>
9#include <QtNetwork/private/qrestaccessmanager_p.h>
10
11#include <QtCore/qbytearrayview.h>
12#include <QtCore/qjsondocument.h>
13#include <QtCore/qlatin1stringmatcher.h>
14#include <QtCore/qlatin1stringview.h>
15#include <QtCore/qloggingcategory.h>
16#include <QtCore/qstringconverter.h>
17
18#include <QtCore/qxpfunctional.h>
19
20QT_BEGIN_NAMESPACE
21
22using namespace Qt::StringLiterals;
23
24/*!
25 \class QRestReply
26 \since 6.7
27 \brief QRestReply is a convenience wrapper for QNetworkReply.
28
29 \reentrant
30 \ingroup network
31 \inmodule QtNetwork
32
33 QRestReply wraps a QNetworkReply and provides convenience methods for data
34 and status handling. The methods provide convenience for typical RESTful
35 client applications.
36
37 QRestReply doesn't take ownership of the wrapped QNetworkReply, and the
38 lifetime and ownership of the reply is as defined by QNetworkAccessManager
39 documentation.
40
41 QRestReply object is not copyable, but is movable.
42
43 \sa QRestAccessManager, QNetworkReply, QNetworkAccessManager,
44 QNetworkAccessManager::setAutoDeleteReplies()
45*/
46
47/*!
48 Creates a QRestReply and initializes the wrapped QNetworkReply to \a reply.
49*/
50QRestReply::QRestReply(QNetworkReply *reply)
51 : wrapped(reply)
52{
53 if (!wrapped)
54 qCWarning(lcQrest, "QRestReply: QNetworkReply is nullptr");
55}
56
57/*!
58 Destroys this QRestReply object.
59*/
60QRestReply::~QRestReply()
61{
62 delete d;
63}
64
65/*!
66 \fn QRestReply::QRestReply(QRestReply &&other) noexcept
67
68 Move-constructs the reply from \a other.
69
70 \note The moved-from object \a other is placed in a
71 partially-formed state, in which the only valid operations are
72 destruction and assignment of a new value.
73*/
74
75/*!
76 \fn QRestReply &QRestReply::operator=(QRestReply &&other) noexcept
77
78 Move-assigns \a other and returns a reference to this reply.
79
80 \note The moved-from object \a other is placed in a
81 partially-formed state, in which the only valid operations are
82 destruction and assignment of a new value.
83*/
84
85/*!
86 Returns a pointer to the underlying QNetworkReply wrapped by this object.
87*/
88QNetworkReply *QRestReply::networkReply() const
89{
90 return wrapped;
91}
92
93/*!
94 Returns the received data as a QJsonDocument.
95
96 The returned value is wrapped in \c std::optional. If the conversion
97 from the received data fails (empty data or JSON parsing error),
98 \c std::nullopt is returned, and \a error is filled with details.
99
100 Calling this function consumes the received data, and any further calls
101 to get response data will return empty.
102
103 This function returns \c {std::nullopt} and will not consume
104 any data if the reply is not finished. If \a error is passed, it will be
105 set to QJsonParseError::NoError to distinguish this case from an actual
106 error.
107
108 \sa readBody(), readText()
109*/
110std::optional<QJsonDocument> QRestReply::readJson(QJsonParseError *error)
111{
112 if (!wrapped) {
113 if (error)
114 *error = {.offset: 0, .error: QJsonParseError::ParseError::NoError};
115 return std::nullopt;
116 }
117
118 if (!wrapped->isFinished()) {
119 qCWarning(lcQrest, "readJson() called on an unfinished reply, ignoring");
120 if (error)
121 *error = {.offset: 0, .error: QJsonParseError::ParseError::NoError};
122 return std::nullopt;
123 }
124 QJsonParseError parseError;
125 const QByteArray data = wrapped->readAll();
126 const QJsonDocument doc = QJsonDocument::fromJson(json: data, error: &parseError);
127 if (error)
128 *error = parseError;
129 if (parseError.error)
130 return std::nullopt;
131 return doc;
132}
133
134/*!
135 Returns the received data as a QByteArray.
136
137 Calling this function consumes the data received so far, and any further
138 calls to get response data will return empty until further data has been
139 received.
140
141 \sa readJson(), readText(), QNetworkReply::bytesAvailable(),
142 QNetworkReply::readyRead()
143*/
144QByteArray QRestReply::readBody()
145{
146 return wrapped ? wrapped->readAll() : QByteArray{};
147}
148
149/*!
150 Returns the received data as a QString.
151
152 The received data is decoded into a QString (UTF-16). If available, the decoding
153 uses the \e Content-Type header's \e charset parameter to determine the
154 source encoding. If the encoding information is not available or not supported
155 by \l QStringConverter, UTF-8 is used by default.
156
157 Calling this function consumes the data received so far. Returns
158 a default constructed value if no new data is available, or if the
159 decoding is not supported by \l QStringConverter, or if the decoding
160 has errors (for example invalid characters).
161
162 \sa readJson(), readBody(), QNetworkReply::readyRead()
163*/
164QString QRestReply::readText()
165{
166 QString result;
167 if (!wrapped)
168 return result;
169
170 QByteArray data = wrapped->readAll();
171 if (data.isEmpty())
172 return result;
173
174 // Text decoding needs to persist decoding state across calls to this function,
175 // so allocate decoder if not yet allocated.
176 if (!d)
177 d = new QRestReplyPrivate;
178
179 if (!d->decoder) {
180 const QByteArray charset = QRestReplyPrivate::contentCharset(reply: wrapped);
181 d->decoder.emplace(args: charset.constData());
182 if (!d->decoder->isValid()) { // the decoder may not support the mimetype's charset
183 qCWarning(lcQrest, "readText(): Charset \"%s\" is not supported", charset.constData());
184 return result;
185 }
186 }
187 // Check if the decoder already had an error, or has errors after decoding current data chunk
188 if (d->decoder->hasError() || (result = (*d->decoder)(data), d->decoder->hasError())) {
189 qCWarning(lcQrest, "readText(): Decoding error occurred");
190 return {};
191 }
192 return result;
193}
194
195/*!
196 Returns the HTTP status received in the server response.
197 The value is \e 0 if not available (the status line has not been received,
198 yet).
199
200 \note The HTTP status is reported as indicated by the received HTTP
201 response. An error() may occur after receiving the status, for instance
202 due to network disconnection while receiving a long response.
203 These potential subsequent errors are not represented by the reported
204 HTTP status.
205
206 \sa isSuccess(), hasError(), error()
207*/
208int QRestReply::httpStatus() const
209{
210 return wrapped ? wrapped->attribute(code: QNetworkRequest::HttpStatusCodeAttribute).toInt() : 0;
211}
212
213/*!
214 \fn bool QRestReply::isSuccess() const
215
216 Returns whether the HTTP status is between 200..299 and no
217 further errors have occurred while receiving the response (for example,
218 abrupt disconnection while receiving the body data). This function
219 is a convenient way to check whether the response is considered successful.
220
221 \sa httpStatus(), hasError(), error()
222*/
223
224/*!
225 Returns whether the HTTP status is between 200..299.
226
227 \sa isSuccess(), httpStatus(), hasError(), error()
228*/
229bool QRestReply::isHttpStatusSuccess() const
230{
231 const int status = httpStatus();
232 return status >= 200 && status < 300;
233}
234
235/*!
236 Returns whether an error has occurred. This includes errors such as
237 network and protocol errors, but excludes cases where the server
238 successfully responded with an HTTP error status (for example
239 \c {500 Internal Server Error}). Use \l httpStatus() or
240 \l isHttpStatusSuccess() to get the HTTP status information.
241
242 \sa httpStatus(), isSuccess(), error(), errorString()
243*/
244bool QRestReply::hasError() const
245{
246 if (!wrapped)
247 return false;
248
249 const int status = httpStatus();
250 if (status > 0) {
251 // The HTTP status is set upon receiving the response headers, but the
252 // connection might still fail later while receiving the body data.
253 return wrapped->error() == QNetworkReply::RemoteHostClosedError;
254 }
255 return wrapped->error() != QNetworkReply::NoError;
256}
257
258/*!
259 Returns the last error, if any. The errors include
260 errors such as network and protocol errors, but exclude
261 cases when the server successfully responded with an HTTP status.
262
263 \sa httpStatus(), isSuccess(), hasError(), errorString()
264*/
265QNetworkReply::NetworkError QRestReply::error() const
266{
267 if (!hasError())
268 return QNetworkReply::NetworkError::NoError;
269 return wrapped->error();
270}
271
272/*!
273 Returns a human-readable description of the last network error.
274
275 \sa httpStatus(), isSuccess(), hasError(), error()
276*/
277QString QRestReply::errorString() const
278{
279 if (hasError())
280 return wrapped->errorString();
281 return {};
282}
283
284QRestReplyPrivate::QRestReplyPrivate()
285 = default;
286
287QRestReplyPrivate::~QRestReplyPrivate()
288 = default;
289
290#ifndef QT_NO_DEBUG_STREAM
291static QLatin1StringView operationName(QNetworkAccessManager::Operation operation)
292{
293 switch (operation) {
294 case QNetworkAccessManager::Operation::GetOperation:
295 return "GET"_L1;
296 case QNetworkAccessManager::Operation::HeadOperation:
297 return "HEAD"_L1;
298 case QNetworkAccessManager::Operation::PostOperation:
299 return "POST"_L1;
300 case QNetworkAccessManager::Operation::PutOperation:
301 return "PUT"_L1;
302 case QNetworkAccessManager::Operation::DeleteOperation:
303 return "DELETE"_L1;
304 case QNetworkAccessManager::Operation::CustomOperation:
305 return "CUSTOM"_L1;
306 case QNetworkAccessManager::Operation::UnknownOperation:
307 return "UNKNOWN"_L1;
308 }
309 Q_UNREACHABLE_RETURN({});
310}
311
312/*!
313 \fn QDebug QRestReply::operator<<(QDebug debug, const QRestReply &reply)
314
315 Writes the \a reply into the \a debug object for debugging purposes.
316
317 \sa {Debugging Techniques}
318*/
319QDebug operator<<(QDebug debug, const QRestReply &reply)
320{
321 const QDebugStateSaver saver(debug);
322 debug.resetFormat().nospace();
323 if (!reply.networkReply()) {
324 debug << "QRestReply(no network reply)";
325 return debug;
326 }
327 debug << "QRestReply(isSuccess = " << reply.isSuccess()
328 << ", httpStatus = " << reply.httpStatus()
329 << ", isHttpStatusSuccess = " << reply.isHttpStatusSuccess()
330 << ", hasError = " << reply.hasError()
331 << ", errorString = " << reply.errorString()
332 << ", error = " << reply.error()
333 << ", isFinished = " << reply.networkReply()->isFinished()
334 << ", bytesAvailable = " << reply.networkReply()->bytesAvailable()
335 << ", url " << reply.networkReply()->url()
336 << ", operation = " << operationName(operation: reply.networkReply()->operation())
337 << ", reply headers = " << reply.networkReply()->headers()
338 << ")";
339 return debug;
340}
341#endif // QT_NO_DEBUG_STREAM
342
343static constexpr auto parse_OWS(QByteArrayView data) noexcept
344{
345 struct R {
346 QByteArrayView ows, tail;
347 };
348
349 constexpr auto is_OWS_char = [](auto ch) { return ch == ' ' || ch == '\t'; };
350
351 qsizetype i = 0;
352 while (i < data.size() && is_OWS_char(data[i]))
353 ++i;
354
355 return R{.ows: data.first(n: i), .tail: data.sliced(pos: i)};
356}
357
358static constexpr void eat_OWS(QByteArrayView &data) noexcept
359{
360 data = parse_OWS(data).tail;
361}
362
363static constexpr auto parse_quoted_string(QByteArrayView data, qxp::function_ref<void(char) const> yield)
364{
365 struct R {
366 QByteArrayView quotedString, tail;
367 constexpr explicit operator bool() const noexcept { return !quotedString.isEmpty(); }
368 };
369
370 if (!data.startsWith(c: '"'))
371 return R{.quotedString: {}, .tail: data};
372
373 qsizetype i = 1; // one past initial DQUOTE
374 while (i < data.size()) {
375 switch (auto ch = data[i++]) {
376 case '"': // final DQUOTE -> end of string
377 return R{.quotedString: data.first(n: i), .tail: data.sliced(pos: i)};
378 case '\\': // quoted-pair
379 // https://www.rfc-editor.org/rfc/rfc9110.html#section-5.6.4-3:
380 // Recipients that process the value of a quoted-string MUST handle a
381 // quoted-pair as if it were replaced by the octet following the backslash.
382 if (i == data.size())
383 break; // premature end
384 ch = data[i++]; // eat '\\'
385 [[fallthrough]];
386 default:
387 // we don't validate quoted-string octets to be only qdtext (Postel's Law)
388 yield(ch);
389 }
390 }
391
392 return R{.quotedString: {}, .tail: data}; // premature end
393}
394
395static constexpr bool is_tchar(char ch) noexcept
396{
397 // ### optimize
398 switch (ch) {
399 case '!':
400 case '#':
401 case '$':
402 case '%':
403 case '&':
404 case '\'':
405 case '*':
406 case '+':
407 case '-':
408 case '.':
409 case '^':
410 case '_':
411 case '`':
412 case '|':
413 case '~':
414 return true;
415 default:
416 return (ch >= 'a' && ch <= 'z')
417 || (ch >= '0' && ch <= '9')
418 || (ch >= 'A' && ch <= 'Z');
419 }
420}
421
422static constexpr auto parse_comment(QByteArrayView data) noexcept
423{
424 struct R {
425 QByteArrayView comment, tail;
426 constexpr explicit operator bool() const noexcept { return !comment.isEmpty(); }
427 };
428
429 const auto invalid = R{.comment: {}, .tail: data}; // preserves original `data`
430
431 // comment = "(" *( ctext / quoted-pair / comment ) ")"
432 // ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
433
434 if (!data.startsWith(c: '('))
435 return invalid;
436
437 qsizetype i = 1;
438 qsizetype level = 1;
439 while (i < data.size()) {
440 switch (data[i++]) {
441 case '(': // nested comment
442 ++level;
443 break;
444 case ')': // end of comment
445 if (--level == 0)
446 return R{.comment: data.first(n: i), .tail: data.sliced(pos: i)};
447 break;
448 case '\\': // quoted-pair
449 if (i == data.size())
450 return invalid; // premature end
451 ++i; // eat escaped character
452 break;
453 default:
454 ; // don't validate ctext - accept everything (Postel's Law)
455 }
456 }
457
458 return invalid; // premature end / unbalanced nesting levels
459}
460
461static constexpr void eat_CWS(QByteArrayView &data) noexcept
462{
463 eat_OWS(data);
464 while (const auto comment = parse_comment(data)) {
465 data = comment.tail;
466 eat_OWS(data);
467 }
468}
469
470static constexpr auto parse_token(QByteArrayView data) noexcept
471{
472 struct R {
473 QByteArrayView token, tail;
474 constexpr explicit operator bool() const noexcept { return !token.isEmpty(); }
475 };
476
477 qsizetype i = 0;
478 while (i < data.size() && is_tchar(ch: data[i]))
479 ++i;
480
481 return R{.token: data.first(n: i), .tail: data.sliced(pos: i)};
482}
483
484static constexpr auto parse_parameter(QByteArrayView data, qxp::function_ref<void(char) const> yield)
485{
486 struct R {
487 QLatin1StringView name; QByteArrayView value; QByteArrayView tail;
488 constexpr explicit operator bool() const noexcept { return !name.isEmpty(); }
489 };
490
491 const auto invalid = R{.name: {}, .value: {}, .tail: data}; // preserves original `data`
492
493 // parameter = parameter-name "=" parameter-value
494 // parameter-name = token
495 // parameter-value = ( token / quoted-string )
496
497 const auto name = parse_token(data);
498 if (!name)
499 return invalid;
500 data = name.tail;
501
502 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
503
504 if (!data.startsWith(c: '='))
505 return invalid;
506 data = data.sliced(pos: 1);
507
508 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
509
510 if (Q_UNLIKELY(data.startsWith('"'))) { // value is a quoted-string
511
512 const auto value = parse_quoted_string(data, yield);
513 if (!value)
514 return invalid;
515 data = value.tail;
516
517 return R{.name: QLatin1StringView{name.token}, .value: value.quotedString, .tail: data};
518
519 } else { // value is a token
520
521 const auto value = parse_token(data);
522 if (!value)
523 return invalid;
524 data = value.tail;
525
526 return R{.name: QLatin1StringView{name.token}, .value: value.token, .tail: data};
527 }
528}
529
530static auto parse_content_type(QByteArrayView data)
531{
532 struct R {
533 QLatin1StringView type, subtype;
534 std::string charset;
535 constexpr explicit operator bool() const noexcept { return !type.isEmpty(); }
536 };
537
538 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
539
540 const auto type = parse_token(data);
541 if (!type)
542 return R{};
543 data = type.tail;
544
545 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
546
547 if (!data.startsWith(c: '/'))
548 return R{};
549 data = data.sliced(pos: 1);
550
551 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
552
553 const auto subtype = parse_token(data);
554 if (!subtype)
555 return R{};
556 data = subtype.tail;
557
558 eat_CWS(data);
559
560 auto r = R{.type: QLatin1StringView{type.token}, .subtype: QLatin1StringView{subtype.token}, .charset: {}};
561
562 while (data.startsWith(c: ';')) {
563
564 data = data.sliced(pos: 1); // eat ';'
565
566 eat_CWS(data);
567
568 const auto param = parse_parameter(data, yield: [&](char ch) { r.charset.append(n: 1, c: ch); });
569 if (param.name.compare(other: "charset"_L1, cs: Qt::CaseInsensitive) == 0) {
570 if (r.charset.empty() && !param.value.startsWith(c: '"')) // wasn't a quoted-string
571 r.charset.assign(first: param.value.begin(), last: param.value.end());
572 return r; // charset found
573 }
574 r.charset.clear(); // wasn't an actual charset
575 if (param.tail.size() == data.size()) // no progress was made
576 break; // returns {type, subtype}
577 // otherwise, continue (accepting e.g. `;;`)
578 data = param.tail;
579
580 eat_CWS(data);
581 }
582
583 return r; // no charset found
584}
585
586QByteArray QRestReplyPrivate::contentCharset(const QNetworkReply* reply)
587{
588 // Content-type consists of mimetype and optional parameters, of which one may be 'charset'
589 // Example values and their combinations below are all valid, see RFC 7231 section 3.1.1.5
590 // and RFC 2045 section 5.1
591 //
592 // text/plain; charset=utf-8
593 // text/plain; charset=utf-8;version=1.7
594 // text/plain; charset = utf-8
595 // text/plain; charset ="utf-8"
596
597 const QByteArray contentTypeValue =
598 reply->headers().value(name: QHttpHeaders::WellKnownHeader::ContentType).toByteArray();
599
600 const auto r = parse_content_type(data: contentTypeValue);
601 if (r && !r.charset.empty())
602 return QByteArrayView(r.charset).toByteArray();
603 else
604 return "UTF-8"_ba; // Default to the most commonly used UTF-8.
605}
606
607QT_END_NAMESPACE
608

source code of qtbase/src/network/access/qrestreply.cpp