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