mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-12-29 01:06:27 -05:00
7365 lines
309 KiB
C++
7365 lines
309 KiB
C++
/*! \file
|
|
*
|
|
* MockNetworkAccessManager
|
|
* https://gitlab.com/julrich/MockNetworkAccessManager
|
|
*
|
|
* \version 0.10.1
|
|
* \author Jochen Ulrich <jochen.ulrich@t-online.de>
|
|
* \copyright © 2018-2022 Jochen Ulrich. Licensed under MIT license (https://opensource.org/licenses/MIT)
|
|
* except for the HttpStatus namespace which is licensed under Creative Commons CC0
|
|
* (http://creativecommons.org/publicdomain/zero/1.0/).
|
|
*/
|
|
/*
|
|
Copyright © 2018-2022 Jochen Ulrich
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
|
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of
|
|
the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
|
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
|
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
#ifndef MOCKNETWORKACCESSMANAGER_HPP
|
|
#define MOCKNETWORKACCESSMANAGER_HPP
|
|
|
|
#include <QtGlobal>
|
|
|
|
#ifdef Q_CC_MSVC
|
|
#pragma warning(push, 0)
|
|
#endif
|
|
|
|
#include <QAtomicInt>
|
|
#include <QtCore>
|
|
#include <QtNetwork>
|
|
|
|
#ifdef Q_CC_MSVC
|
|
#pragma warning(pop)
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#include <climits>
|
|
#include <memory>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
#if defined(QT_CORE5COMPAT_LIB)
|
|
#include <QtCore5Compat>
|
|
#endif
|
|
#endif // Qt >= 6.0.0
|
|
|
|
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) || (defined(QT_FEATURE_textcodec) && QT_FEATURE_textcodec == 1)
|
|
/*! Defined if the QTextCodec is available.
|
|
*
|
|
* This is if %Qt version is below 6.0.0 or if the QtCore5Compat library is linked.
|
|
*/
|
|
#define MOCKNETWORKACCESSMANAGER_QT_HAS_TEXTCODEC
|
|
#endif
|
|
|
|
//! \cond QT_POLYFILL
|
|
#ifndef Q_NAMESPACE
|
|
#define Q_NAMESPACE
|
|
#endif
|
|
|
|
#ifndef Q_ENUM_NS
|
|
#define Q_ENUM_NS(x)
|
|
#endif
|
|
|
|
#ifndef Q_DECL_DEPRECATED_X
|
|
#define Q_DECL_DEPRECATED_X(x)
|
|
#endif
|
|
//! \endcond
|
|
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
|
|
namespace Qt
|
|
{
|
|
typedef QString::SplitBehavior SplitBehaviorFlags;
|
|
} // namespace Qt
|
|
#endif // Qt < 5.14.0
|
|
|
|
/*! Provides functions and classes to mock network replies in %Qt applications.
|
|
*/
|
|
namespace MockNetworkAccess
|
|
{
|
|
|
|
Q_NAMESPACE
|
|
|
|
/*! Returns the logging category used by the library.
|
|
*
|
|
* The name of the category is `MockNetworkAccessManager`.
|
|
* All messages logged by the library use this logging category.
|
|
*
|
|
* \return The QLoggingCategory of the MockNetworkAccessManager library.
|
|
*/
|
|
inline Q_LOGGING_CATEGORY(log, "MockNetworkAccessManager")
|
|
|
|
/*! Behavior switches defining different behaviors for the classes of the Manager.
|
|
* \sa page_behaviorFlags
|
|
*/
|
|
enum BehaviorFlag {
|
|
/*! Defines that a class behaves as expected according to the documentation and standards
|
|
* (RFCs etc.). This also means it should behave like most %Qt bugs being fixed
|
|
* (see \ref page_knownQtBugs for a list of exceptions).
|
|
* This flag cannot be combined with other BehaviorFlags.
|
|
* \sa \ref page_knownQtBugs
|
|
*/
|
|
Behavior_Expected = 0,
|
|
|
|
/*! Defines that the MockReplies emits an `uploadProgress(0, 0)` signal after the download.
|
|
* There is QTBUG-44782 in QNetworkReply which causes it to emit an `uploadProgress(0, 0)` signal after
|
|
* the download of the reply has finished.
|
|
* \sa https://bugreports.qt.io/browse/QTBUG-44782
|
|
*/
|
|
Behavior_FinalUpload00Signal = 1 << 1,
|
|
/*! Defines that the Manager does not automatically redirect on 308 "Permanent Redirect" responses.
|
|
* %Qt does not respect the 308 status code for automatic redirection up to %Qt 5.9.3 (was fixed with
|
|
* QTBUG-63075). \sa https://bugreports.qt.io/browse/QTBUG-63075 \since 0.3.0
|
|
*/
|
|
Behavior_NoAutomatic308Redirect = 1 << 2,
|
|
/*! Defines that the Manager follows all redirects with a GET request (except the initial request was a HEAD
|
|
* request in which case it follows with a HEAD request as well).
|
|
* %Qt up to 5.9.3 followed all redirected requests except HEAD requests with a GET request. QTBUG-63142
|
|
* fixes this to not change the request method for 307 and 308 requests. \sa
|
|
* https://bugreports.qt.io/browse/QTBUG-63142 \since 0.3.0
|
|
*/
|
|
Behavior_RedirectWithGet = 1 << 3,
|
|
/*! Defines that the Manager assumes Latin-1 encoding for username and password for HTTP authentication.
|
|
* By default, the Manager uses the `charset` parameter of the authentication scheme and defaults to UTF-8
|
|
* encoding.
|
|
*/
|
|
Behavior_HttpAuthLatin1Encoding = 1 << 4,
|
|
/*! Defines that the Manager rewrites the request verbs OPTIONS and TRACE to GET when following redirects.
|
|
* [RFC 7231, Section 4.2.1](https://tools.ietf.org/html/rfc7231#section-4.2.1) defines the HTTP verbs
|
|
* OPTIONS and TRACE as "safe" request methods so it should be fine to use them when automatically following
|
|
* redirects for HTTP status codes 301 and 302. This behavior defines that the Manager should still redirect
|
|
* with a GET request in that case. \note Behavior_RedirectWithGet overrides this flag. So if
|
|
* Behavior_RedirectWithGet is set, this flag is ignored. \since 0.3.0
|
|
*/
|
|
Behavior_IgnoreSafeRedirectMethods = 1 << 5,
|
|
|
|
/*! Defines the behavior of %Qt 5.6.
|
|
*/
|
|
Behavior_Qt_5_6_0 = Behavior_FinalUpload00Signal | Behavior_NoAutomatic308Redirect
|
|
| Behavior_RedirectWithGet | Behavior_HttpAuthLatin1Encoding
|
|
| Behavior_IgnoreSafeRedirectMethods,
|
|
/*! Defines the behavior of %Qt 5.2.
|
|
* \since 0.3.0
|
|
*/
|
|
Behavior_Qt_5_2_0 = Behavior_Qt_5_6_0
|
|
};
|
|
/*! QFlags type of \ref BehaviorFlag
|
|
*/
|
|
Q_DECLARE_FLAGS(BehaviorFlags, BehaviorFlag)
|
|
Q_ENUM_NS(BehaviorFlag)
|
|
|
|
// LCOV_EXCL_START
|
|
// @sonarcloud-exclude-start
|
|
/*! HTTP Status Codes - Qt Variant
|
|
*
|
|
* https://github.com/j-ulrich/http-status-codes-cpp
|
|
*
|
|
* \version 1.5.0
|
|
* \author Jochen Ulrich <jochenulrich@t-online.de>
|
|
* \copyright Licensed under Creative Commons CC0 (http://creativecommons.org/publicdomain/zero/1.0/)
|
|
*/
|
|
namespace HttpStatus
|
|
{
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
|
|
Q_NAMESPACE
|
|
#endif
|
|
enum Code
|
|
{
|
|
Continue = 100,
|
|
SwitchingProtocols = 101,
|
|
Processing = 102,
|
|
EarlyHints = 103,
|
|
OK = 200,
|
|
Created = 201,
|
|
Accepted = 202,
|
|
NonAuthoritativeInformation = 203,
|
|
NoContent = 204,
|
|
ResetContent = 205,
|
|
PartialContent = 206,
|
|
MultiStatus = 207,
|
|
AlreadyReported = 208,
|
|
IMUsed = 226,
|
|
MultipleChoices = 300,
|
|
MovedPermanently = 301,
|
|
Found = 302,
|
|
SeeOther = 303,
|
|
NotModified = 304,
|
|
UseProxy = 305,
|
|
TemporaryRedirect = 307,
|
|
PermanentRedirect = 308,
|
|
BadRequest = 400,
|
|
Unauthorized = 401,
|
|
PaymentRequired = 402,
|
|
Forbidden = 403,
|
|
NotFound = 404,
|
|
MethodNotAllowed = 405,
|
|
NotAcceptable = 406,
|
|
ProxyAuthenticationRequired = 407,
|
|
RequestTimeout = 408,
|
|
Conflict = 409,
|
|
Gone = 410,
|
|
LengthRequired = 411,
|
|
PreconditionFailed = 412,
|
|
ContentTooLarge = 413,
|
|
PayloadTooLarge = 413,
|
|
URITooLong = 414,
|
|
UnsupportedMediaType = 415,
|
|
RangeNotSatisfiable = 416,
|
|
ExpectationFailed = 417,
|
|
ImATeapot = 418,
|
|
MisdirectedRequest = 421,
|
|
UnprocessableContent = 422,
|
|
UnprocessableEntity = 422,
|
|
Locked = 423,
|
|
FailedDependency = 424,
|
|
TooEarly = 425,
|
|
UpgradeRequired = 426,
|
|
PreconditionRequired = 428,
|
|
TooManyRequests = 429,
|
|
RequestHeaderFieldsTooLarge = 431,
|
|
UnavailableForLegalReasons = 451,
|
|
InternalServerError = 500,
|
|
NotImplemented = 501,
|
|
BadGateway = 502,
|
|
ServiceUnavailable = 503,
|
|
GatewayTimeout = 504,
|
|
HTTPVersionNotSupported = 505,
|
|
VariantAlsoNegotiates = 506,
|
|
InsufficientStorage = 507,
|
|
LoopDetected = 508,
|
|
NotExtended = 510,
|
|
NetworkAuthenticationRequired = 511,
|
|
xxx_max = 1023
|
|
};
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
|
|
Q_ENUM_NS(Code)
|
|
#endif
|
|
inline bool isInformational(int code)
|
|
{
|
|
return (code >= 100 && code < 200);
|
|
}
|
|
inline bool isSuccessful(int code)
|
|
{
|
|
return (code >= 200 && code < 300);
|
|
}
|
|
inline bool isRedirection(int code)
|
|
{
|
|
return (code >= 300 && code < 400);
|
|
}
|
|
inline bool isClientError(int code)
|
|
{
|
|
return (code >= 400 && code < 500);
|
|
}
|
|
inline bool isServerError(int code)
|
|
{
|
|
return (code >= 500 && code < 600);
|
|
}
|
|
inline bool isError(int code)
|
|
{
|
|
return (code >= 400);
|
|
}
|
|
inline QString reasonPhrase(int code)
|
|
{
|
|
switch (code) {
|
|
case 100:
|
|
return QStringLiteral("Continue");
|
|
case 101:
|
|
return QStringLiteral("Switching Protocols");
|
|
case 102:
|
|
return QStringLiteral("Processing");
|
|
case 103:
|
|
return QStringLiteral("Early Hints");
|
|
case 200:
|
|
return QStringLiteral("OK");
|
|
case 201:
|
|
return QStringLiteral("Created");
|
|
case 202:
|
|
return QStringLiteral("Accepted");
|
|
case 203:
|
|
return QStringLiteral("Non-Authoritative Information");
|
|
case 204:
|
|
return QStringLiteral("No Content");
|
|
case 205:
|
|
return QStringLiteral("Reset Content");
|
|
case 206:
|
|
return QStringLiteral("Partial Content");
|
|
case 207:
|
|
return QStringLiteral("Multi-Status");
|
|
case 208:
|
|
return QStringLiteral("Already Reported");
|
|
case 226:
|
|
return QStringLiteral("IM Used");
|
|
case 300:
|
|
return QStringLiteral("Multiple Choices");
|
|
case 301:
|
|
return QStringLiteral("Moved Permanently");
|
|
case 302:
|
|
return QStringLiteral("Found");
|
|
case 303:
|
|
return QStringLiteral("See Other");
|
|
case 304:
|
|
return QStringLiteral("Not Modified");
|
|
case 305:
|
|
return QStringLiteral("Use Proxy");
|
|
case 307:
|
|
return QStringLiteral("Temporary Redirect");
|
|
case 308:
|
|
return QStringLiteral("Permanent Redirect");
|
|
case 400:
|
|
return QStringLiteral("Bad Request");
|
|
case 401:
|
|
return QStringLiteral("Unauthorized");
|
|
case 402:
|
|
return QStringLiteral("Payment Required");
|
|
case 403:
|
|
return QStringLiteral("Forbidden");
|
|
case 404:
|
|
return QStringLiteral("Not Found");
|
|
case 405:
|
|
return QStringLiteral("Method Not Allowed");
|
|
case 406:
|
|
return QStringLiteral("Not Acceptable");
|
|
case 407:
|
|
return QStringLiteral("Proxy Authentication Required");
|
|
case 408:
|
|
return QStringLiteral("Request Timeout");
|
|
case 409:
|
|
return QStringLiteral("Conflict");
|
|
case 410:
|
|
return QStringLiteral("Gone");
|
|
case 411:
|
|
return QStringLiteral("Length Required");
|
|
case 412:
|
|
return QStringLiteral("Precondition Failed");
|
|
case 413:
|
|
return QStringLiteral("Content Too Large");
|
|
case 414:
|
|
return QStringLiteral("URI Too Long");
|
|
case 415:
|
|
return QStringLiteral("Unsupported Media Type");
|
|
case 416:
|
|
return QStringLiteral("Range Not Satisfiable");
|
|
case 417:
|
|
return QStringLiteral("Expectation Failed");
|
|
case 418:
|
|
return QStringLiteral("I'm a teapot");
|
|
case 421:
|
|
return QStringLiteral("Misdirected Request");
|
|
case 422:
|
|
return QStringLiteral("Unprocessable Content");
|
|
case 423:
|
|
return QStringLiteral("Locked");
|
|
case 424:
|
|
return QStringLiteral("Failed Dependency");
|
|
case 425:
|
|
return QStringLiteral("Too Early");
|
|
case 426:
|
|
return QStringLiteral("Upgrade Required");
|
|
case 428:
|
|
return QStringLiteral("Precondition Required");
|
|
case 429:
|
|
return QStringLiteral("Too Many Requests");
|
|
case 431:
|
|
return QStringLiteral("Request Header Fields Too Large");
|
|
case 451:
|
|
return QStringLiteral("Unavailable For Legal Reasons");
|
|
case 500:
|
|
return QStringLiteral("Internal Server Error");
|
|
case 501:
|
|
return QStringLiteral("Not Implemented");
|
|
case 502:
|
|
return QStringLiteral("Bad Gateway");
|
|
case 503:
|
|
return QStringLiteral("Service Unavailable");
|
|
case 504:
|
|
return QStringLiteral("Gateway Timeout");
|
|
case 505:
|
|
return QStringLiteral("HTTP Version Not Supported");
|
|
case 506:
|
|
return QStringLiteral("Variant Also Negotiates");
|
|
case 507:
|
|
return QStringLiteral("Insufficient Storage");
|
|
case 508:
|
|
return QStringLiteral("Loop Detected");
|
|
case 510:
|
|
return QStringLiteral("Not Extended");
|
|
case 511:
|
|
return QStringLiteral("Network Authentication Required");
|
|
default:
|
|
return QString();
|
|
}
|
|
}
|
|
inline int networkErrorToStatusCode(QNetworkReply::NetworkError error)
|
|
{
|
|
switch (error) {
|
|
case QNetworkReply::AuthenticationRequiredError:
|
|
return Unauthorized;
|
|
case QNetworkReply::ContentAccessDenied:
|
|
return Forbidden;
|
|
case QNetworkReply::ContentNotFoundError:
|
|
return NotFound;
|
|
case QNetworkReply::ContentOperationNotPermittedError:
|
|
return MethodNotAllowed;
|
|
case QNetworkReply::ProxyAuthenticationRequiredError:
|
|
return ProxyAuthenticationRequired;
|
|
case QNetworkReply::NoError:
|
|
return OK;
|
|
case QNetworkReply::ProtocolInvalidOperationError:
|
|
return BadRequest;
|
|
case QNetworkReply::UnknownContentError:
|
|
return BadRequest;
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 3, 0)
|
|
case QNetworkReply::ContentConflictError:
|
|
return Conflict;
|
|
case QNetworkReply::ContentGoneError:
|
|
return Gone;
|
|
case QNetworkReply::InternalServerError:
|
|
return InternalServerError;
|
|
case QNetworkReply::OperationNotImplementedError:
|
|
return NotImplemented;
|
|
case QNetworkReply::ServiceUnavailableError:
|
|
return ServiceUnavailable;
|
|
case QNetworkReply::UnknownServerError:
|
|
return InternalServerError;
|
|
#endif
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
inline QNetworkReply::NetworkError statusCodeToNetworkError(int code)
|
|
{
|
|
if (!isError(code))
|
|
return QNetworkReply::NoError;
|
|
switch (code) {
|
|
case BadRequest:
|
|
return QNetworkReply::ProtocolInvalidOperationError;
|
|
case Unauthorized:
|
|
return QNetworkReply::AuthenticationRequiredError;
|
|
case Forbidden:
|
|
return QNetworkReply::ContentAccessDenied;
|
|
case NotFound:
|
|
return QNetworkReply::ContentNotFoundError;
|
|
case MethodNotAllowed:
|
|
return QNetworkReply::ContentOperationNotPermittedError;
|
|
case ProxyAuthenticationRequired:
|
|
return QNetworkReply::ProxyAuthenticationRequiredError;
|
|
case ImATeapot:
|
|
return QNetworkReply::ProtocolInvalidOperationError;
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 3, 0)
|
|
case Conflict:
|
|
return QNetworkReply::ContentConflictError;
|
|
case Gone:
|
|
return QNetworkReply::ContentGoneError;
|
|
case InternalServerError:
|
|
return QNetworkReply::InternalServerError;
|
|
case NotImplemented:
|
|
return QNetworkReply::OperationNotImplementedError;
|
|
case ServiceUnavailable:
|
|
return QNetworkReply::ServiceUnavailableError;
|
|
#endif
|
|
default:
|
|
break;
|
|
}
|
|
if (isClientError(code))
|
|
return QNetworkReply::UnknownContentError;
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 3, 0)
|
|
if (isServerError(code))
|
|
return QNetworkReply::UnknownServerError;
|
|
#endif
|
|
return QNetworkReply::ProtocolFailure;
|
|
}
|
|
} // namespace HttpStatus
|
|
// @sonarcloud-exclude-end
|
|
// LCOV_EXCL_STOP
|
|
|
|
/*! \internal Implementation details
|
|
*/
|
|
namespace detail
|
|
{
|
|
|
|
/*! \internal
|
|
* Formats a pointer's address as string.
|
|
* \param pointer The pointer.
|
|
* \return A string representing the \p pointer's address.
|
|
*/
|
|
inline QString pointerToQString(const void* pointer)
|
|
{
|
|
// From https://stackoverflow.com/a/16568641/490560
|
|
const int bytesPerHexDigit = 2;
|
|
const int hexBase = 16;
|
|
return QString::fromLatin1("0x%1").arg(reinterpret_cast<quintptr>(pointer),
|
|
QT_POINTER_SIZE * bytesPerHexDigit,
|
|
hexBase,
|
|
QChar::fromLatin1('0'));
|
|
}
|
|
|
|
} // namespace detail
|
|
|
|
/*! Provides helper methods for tasks related to HTTP.
|
|
*
|
|
* \sa https://tools.ietf.org/html/rfc7230
|
|
*/
|
|
namespace HttpUtils
|
|
{
|
|
/*! The default port of HTTP requests.
|
|
*/
|
|
const int HttpDefaultPort = 80;
|
|
|
|
/*! The default port of HTTPS requests.
|
|
*/
|
|
const int HttpsDefaultPort = 443;
|
|
|
|
/*! \return The scheme of the Hypertext Transfer Protocol (HTTP) in lower case characters.
|
|
*/
|
|
inline QString httpScheme()
|
|
{
|
|
const QString httpSchemeString = QStringLiteral("http");
|
|
return httpSchemeString;
|
|
}
|
|
|
|
/*! \return The scheme of the Hypertext Transfer Protocol Secure (HTTPS) in lower case characters.
|
|
*/
|
|
inline QString httpsScheme()
|
|
{
|
|
const QString httpsSchemeString = QStringLiteral("https");
|
|
return httpsSchemeString;
|
|
}
|
|
|
|
/*! \return The name of the Location header field.
|
|
*/
|
|
inline QByteArray locationHeader()
|
|
{
|
|
const QByteArray locationHeaderKey = QByteArrayLiteral("Location");
|
|
return locationHeaderKey;
|
|
}
|
|
|
|
/*! \return The name of the WWW-Authenticate header field.
|
|
*/
|
|
inline QByteArray wwwAuthenticateHeader()
|
|
{
|
|
const QByteArray wwwAuthenticateHeaderKey = QByteArrayLiteral("WWW-Authenticate");
|
|
return wwwAuthenticateHeaderKey;
|
|
}
|
|
|
|
/*! \return The name of the Proxy-Authenticate header field.
|
|
*/
|
|
inline QByteArray proxyAuthenticateHeader()
|
|
{
|
|
const QByteArray proxyAuthenticateHeaderKey = QByteArrayLiteral("Proxy-Authenticate");
|
|
return proxyAuthenticateHeaderKey;
|
|
}
|
|
|
|
/*! \return The name of the Authorization header field.
|
|
*/
|
|
inline QByteArray authorizationHeader()
|
|
{
|
|
const QByteArray authorizationHeaderKey = QByteArrayLiteral("Authorization");
|
|
return authorizationHeaderKey;
|
|
}
|
|
|
|
/*! \return The name of the Proxy-Authorization header field.
|
|
*/
|
|
inline QByteArray proxyAuthorizationHeader()
|
|
{
|
|
const QByteArray proxyAuthorizationHeaderKey = QByteArrayLiteral("Proxy-Authorization");
|
|
return proxyAuthorizationHeaderKey;
|
|
}
|
|
|
|
/*! \return The regular expression pattern to match tokens according to RFC 7230 3.2.6.
|
|
*/
|
|
inline QString tokenPattern()
|
|
{
|
|
const QString token = QStringLiteral("(?:[0-9a-zA-Z!#$%&'*+\\-.^_`|~]+)");
|
|
return token;
|
|
}
|
|
|
|
/*! \return The regular expression pattern to match token68 according to RFC 7235 2.1.
|
|
*/
|
|
inline QString token68Pattern()
|
|
{
|
|
const QString token68 = QStringLiteral("(?:[0-9a-zA-Z\\-._~+\\/]+=*)");
|
|
return token68;
|
|
}
|
|
/*! \return The regular expression pattern to match successive linear whitespace according to RFC 7230 3.2.3.
|
|
*/
|
|
inline QString lwsPattern()
|
|
{
|
|
const QString lws = QStringLiteral("(?:[ \t]+)");
|
|
return lws;
|
|
}
|
|
/*! \return The regular expression pattern to match obsolete line folding (obs-fold) according to RFC
|
|
* 7230 3.2.4.
|
|
*/
|
|
inline QString obsFoldPattern()
|
|
{
|
|
const QString obsFold = QStringLiteral("(?:\r\n[ \t])");
|
|
return obsFold;
|
|
}
|
|
/*! Returns a version of a string with linear whitespace according to RFC 7230 3.2.3 removed from the
|
|
* beginning and end of the string.
|
|
*
|
|
* \param string The string whose leading and trailing linear whitespace should be removed.
|
|
* \return A copy of \p string with all horizontal tabs and spaces removed from the beginning and end of the
|
|
* string.
|
|
*/
|
|
inline QString trimmed(const QString& string)
|
|
{
|
|
const QRegularExpression leadingLwsRegEx(QStringLiteral("^") + lwsPattern() + QStringLiteral("+"));
|
|
const QRegularExpression trailingLwsRegEx(lwsPattern() + QStringLiteral("+$"));
|
|
|
|
QString trimmed(string);
|
|
|
|
const QRegularExpressionMatch leadingMatch = leadingLwsRegEx.match(trimmed);
|
|
if (leadingMatch.hasMatch())
|
|
trimmed.remove(0, leadingMatch.capturedLength(0));
|
|
|
|
const QRegularExpressionMatch trailingMatch = trailingLwsRegEx.match(trimmed);
|
|
if (trailingMatch.hasMatch())
|
|
trimmed.remove(trailingMatch.capturedStart(0), trailingMatch.capturedLength(0));
|
|
|
|
return trimmed;
|
|
}
|
|
/*! Returns a version of a string with obsolete line folding replaced with a space and whitespace trimmed,
|
|
* both according to RFC 7230.
|
|
*
|
|
* \param string The string which should be trimmed and whose obs-folding should be removed.
|
|
* \return A copy of \p string with all obsolete line foldings (RFC 7230 3.2.4) replaced with a space
|
|
* and afterwards, trimmed using trimmed().
|
|
*
|
|
* \sa trimmed()
|
|
*/
|
|
inline QString whiteSpaceCleaned(const QString& string)
|
|
{
|
|
const QRegularExpression obsFoldRegEx(obsFoldPattern());
|
|
QString cleaned(string);
|
|
cleaned.replace(obsFoldRegEx, QLatin1String(" "));
|
|
return trimmed(cleaned);
|
|
}
|
|
|
|
/*! Checks if a given string is a token according to RFC 7230 3.2.6.
|
|
*
|
|
* \param string The string to be checked to be a token.
|
|
* \return \c true if \p string is a valid token or \c false otherwise.
|
|
*/
|
|
inline bool isValidToken(const QString& string)
|
|
{
|
|
const QRegularExpression tokenRegEx(QStringLiteral("^") + tokenPattern() + QStringLiteral("$"));
|
|
return tokenRegEx.match(string).hasMatch();
|
|
}
|
|
|
|
/*! Checks if a character is a visible (printable) US ASCII character.
|
|
*
|
|
* @param character The character to be checked.
|
|
* @return \c true if \p character is a printable US ASCII character.
|
|
*/
|
|
inline bool isVCHAR(const char character)
|
|
{
|
|
const char FirstVCHAR = '\x21';
|
|
const char LastVCHAR = '\x7E';
|
|
|
|
return character >= FirstVCHAR && character <= LastVCHAR;
|
|
}
|
|
|
|
/*! Checks if a character is an "obs-text" character according to RFC 7230 3.2.6.
|
|
*
|
|
* @param character The character to be checked.
|
|
* @return \c true if \p character falls into the "obs-text" character range.
|
|
*/
|
|
inline bool isObsTextChar(const char character)
|
|
{
|
|
#if CHAR_MIN < 0
|
|
// char is signed so all obs-text characters are negative
|
|
return character < 0;
|
|
#else
|
|
const char FirstObsTextChar = '\x80';
|
|
|
|
/* LastObsTextChar would be 0xFF which is the maximum value of char
|
|
* so there is no need to check if character is smaller.
|
|
*/
|
|
|
|
return character >= FirstObsTextChar;
|
|
#endif
|
|
}
|
|
|
|
/*! Checks if a character is legal to occur in a header field according to RFC 7230 3.2.6.
|
|
*
|
|
* \param character The character to be checked.
|
|
* \return \c true if \p character is an allowed character for a header field value.
|
|
*/
|
|
inline bool isLegalHeaderCharacter(const char character)
|
|
{
|
|
return (character == QChar::Tabulation || character == QChar::Space || isVCHAR(character)
|
|
|| isObsTextChar(character));
|
|
}
|
|
|
|
/*! Checks if a string is a valid quoted-string according to RFC 7230 3.2.6.
|
|
*
|
|
* \param string The string to be tested. \p string is expected to *not* contain obsolete line folding
|
|
* (obs-fold). Use whiteSpaceCleaned() to ensure this. \return \c true if the \p string is a valid
|
|
* quoted-string.
|
|
*/
|
|
inline bool isValidQuotedString(const QString& string)
|
|
{
|
|
// String needs to contain at least the quotes
|
|
const int minimumStringSize = 2;
|
|
if (string.size() < minimumStringSize)
|
|
return false;
|
|
|
|
// First character must be a quote
|
|
if (string.at(0).toLatin1() != '"')
|
|
return false;
|
|
|
|
unsigned int backslashCount = 0;
|
|
const int backslashEscapeLength = 2;
|
|
for (int i = 1, stringContentEnd = string.size() - 1; i < stringContentEnd; ++i) {
|
|
// Non-Latin-1 characters will be 0
|
|
const char c = string.at(i).toLatin1();
|
|
|
|
// String must not contain illegal characters
|
|
if (Q_UNLIKELY(!isLegalHeaderCharacter(c)))
|
|
return false;
|
|
|
|
if (c == '\\')
|
|
++backslashCount;
|
|
else {
|
|
// Other quotes and obs-text must be escaped
|
|
if ((c == '"' || isObsTextChar(c)) && (backslashCount % backslashEscapeLength) == 0)
|
|
return false;
|
|
|
|
backslashCount = 0;
|
|
}
|
|
}
|
|
|
|
// Last character must be a quote and it must not be escaped
|
|
if (string.at(string.size() - 1).toLatin1() != '"' || (backslashCount % backslashEscapeLength) != 0)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/*! Converts a quoted-string according to RFC 7230 3.2.6 to it's unquoted version.
|
|
*
|
|
* \param quotedString The quoted string to be converted to "plain" text.
|
|
* \return A copy of \p quotedString with all quoted-pairs converted to the second character of the pair and the
|
|
* leading and trailing double quotes removed. If \p quotedString is not a valid quoted-string, a null
|
|
* QString() is returned.
|
|
*/
|
|
inline QString unquoteString(const QString& quotedString)
|
|
{
|
|
if (!isValidQuotedString(quotedString))
|
|
return QString();
|
|
|
|
QString unquotedString(quotedString.mid(1, quotedString.size() - 2));
|
|
|
|
const QRegularExpression quotedPairRegEx(QStringLiteral("\\\\."));
|
|
QStack<QRegularExpressionMatch> quotedPairMatches;
|
|
QRegularExpressionMatchIterator quotedPairIter = quotedPairRegEx.globalMatch(unquotedString);
|
|
while (quotedPairIter.hasNext())
|
|
quotedPairMatches.push(quotedPairIter.next());
|
|
|
|
while (!quotedPairMatches.isEmpty()) {
|
|
const QRegularExpressionMatch match = quotedPairMatches.pop();
|
|
unquotedString.remove(match.capturedStart(0), 1);
|
|
}
|
|
|
|
return unquotedString;
|
|
}
|
|
|
|
/*! Converts a string to it's quoted version according to RFC 7230 3.2.6.
|
|
*
|
|
* \param unquotedString The "plain" text to be converted to a quoted-string.
|
|
* \return A copy of \p unquotedString surrounded with double quotes and all double quotes, backslashes
|
|
* and obs-text characters escaped. If the \p unquotedString contains any characters that are not allowed
|
|
* in a header field value, a null QString() is returned.
|
|
*/
|
|
inline QString quoteString(const QString& unquotedString)
|
|
{
|
|
QString escapedString;
|
|
|
|
for (int i = 0, unquotedStringSize = unquotedString.size(); i < unquotedStringSize; ++i) {
|
|
// Non-Latin-1 characters will be 0
|
|
const char c = unquotedString.at(i).toLatin1();
|
|
|
|
if (Q_UNLIKELY(!isLegalHeaderCharacter(c)))
|
|
return QString();
|
|
|
|
if (c == '"' || c == '\\' || isObsTextChar(c))
|
|
escapedString += QChar::fromLatin1('\\');
|
|
escapedString += QChar::fromLatin1(c);
|
|
}
|
|
|
|
return QStringLiteral("\"") + escapedString + QStringLiteral("\"");
|
|
}
|
|
|
|
/*! \internal Implementation details
|
|
*/
|
|
namespace detail
|
|
{
|
|
class CommaSeparatedListParser
|
|
{
|
|
public:
|
|
CommaSeparatedListParser()
|
|
: m_inString(false)
|
|
, m_escaped(false)
|
|
{
|
|
}
|
|
|
|
QStringList parse(const QString& commaSeparatedList)
|
|
{
|
|
QString::const_iterator iter = commaSeparatedList.cbegin();
|
|
const QString::const_iterator end = commaSeparatedList.cend();
|
|
for (; iter != end; ++iter) {
|
|
processCharacter(*iter);
|
|
}
|
|
|
|
if (!checkStateAfterParsing())
|
|
return QStringList();
|
|
|
|
finalizeNextEntry();
|
|
|
|
return m_split;
|
|
}
|
|
|
|
private:
|
|
void processCharacter(QChar character)
|
|
{
|
|
if (m_inString)
|
|
processCharacterInString(character);
|
|
else
|
|
processCharacterOutsideString(character);
|
|
}
|
|
|
|
void processCharacterInString(QChar character)
|
|
{
|
|
if (character == QChar::fromLatin1('\\'))
|
|
m_escaped = !m_escaped;
|
|
else {
|
|
if (character == QChar::fromLatin1('"') && !m_escaped)
|
|
m_inString = false;
|
|
m_escaped = false;
|
|
}
|
|
m_nextEntry += character;
|
|
}
|
|
|
|
void processCharacterOutsideString(QChar character)
|
|
{
|
|
if (character == QChar::fromLatin1(',')) {
|
|
finalizeNextEntry();
|
|
} else {
|
|
if (character == QChar::fromLatin1('"'))
|
|
m_inString = true;
|
|
m_nextEntry += character;
|
|
}
|
|
}
|
|
|
|
void finalizeNextEntry()
|
|
{
|
|
const QString trimmedEntry = trimmed(m_nextEntry);
|
|
if (!trimmedEntry.isEmpty())
|
|
m_split << trimmedEntry;
|
|
m_nextEntry.clear();
|
|
}
|
|
|
|
bool checkStateAfterParsing() const
|
|
{
|
|
return !m_inString;
|
|
}
|
|
|
|
private:
|
|
bool m_inString;
|
|
bool m_escaped;
|
|
QString m_nextEntry;
|
|
QStringList m_split;
|
|
};
|
|
} // namespace detail
|
|
|
|
/*! Splits a string containing a comma-separated list according to RFC 7230 section 7.
|
|
*
|
|
* \param commaSeparatedList A string containing a comma-separated list. The list can contain
|
|
* quoted strings and commas within quoted strings are not treated as list separators.
|
|
* \return QStringList consisting of the elements of \p commaSeparatedList.
|
|
* Empty elements in \p commaSeparatedList are omitted.
|
|
*/
|
|
inline QStringList splitCommaSeparatedList(const QString& commaSeparatedList)
|
|
{
|
|
detail::CommaSeparatedListParser parser;
|
|
return parser.parse(commaSeparatedList);
|
|
}
|
|
|
|
/*! Namespace for HTTP authentication related classes.
|
|
*
|
|
* \sa https://tools.ietf.org/html/rfc7235
|
|
*/
|
|
namespace Authentication
|
|
{
|
|
/*! Returns the authentication scope of a URL according to RFC 7617 2.2.
|
|
*
|
|
* \param url The URL whose authentication scope should be returned.
|
|
* \return A URL which has the same scheme, host, port and path up to the last slash
|
|
* as \p url.
|
|
*/
|
|
inline QUrl authenticationScopeForUrl(const QUrl& url)
|
|
{
|
|
QUrl authScope;
|
|
authScope.setScheme(url.scheme());
|
|
authScope.setHost(url.host());
|
|
authScope.setPort(url.port());
|
|
const QFileInfo urlPath(url.path(QUrl::FullyEncoded));
|
|
QString path = urlPath.path(); // Remove the part after the last slash using QFileInfo::path()
|
|
if (path.isEmpty() || path == QLatin1String("."))
|
|
path = QLatin1String("/");
|
|
else if (!path.endsWith(QChar::fromLatin1('/')))
|
|
path += QChar::fromLatin1('/');
|
|
authScope.setPath(path);
|
|
return authScope;
|
|
}
|
|
|
|
/*! \internal Implementation details
|
|
*/
|
|
namespace detail
|
|
{
|
|
|
|
inline QByteArray authorizationHeaderKey()
|
|
{
|
|
const QByteArray authHeader = QByteArrayLiteral("Authorization");
|
|
return authHeader;
|
|
}
|
|
|
|
inline QString authParamPattern()
|
|
{
|
|
const QString authParam = QStringLiteral("(?:(?<authParamName>") + HttpUtils::tokenPattern()
|
|
+ QStringLiteral(")") + HttpUtils::lwsPattern() + QStringLiteral("?")
|
|
+ QStringLiteral("=") + HttpUtils::lwsPattern() + QStringLiteral("?")
|
|
+ QStringLiteral("(?<authParamValue>") + HttpUtils::tokenPattern()
|
|
+ QStringLiteral("|\".*\"))");
|
|
return authParam;
|
|
}
|
|
|
|
} // namespace detail
|
|
|
|
/*! Represents an HTTP authentication challenge according to RFC 7235.
|
|
*/
|
|
class Challenge
|
|
{
|
|
public:
|
|
/*! QSharedPointer to a Challenge object.
|
|
*/
|
|
typedef QSharedPointer<Challenge> Ptr;
|
|
|
|
/*! Defines the supported HTTP authentication schemes.
|
|
*/
|
|
enum AuthenticationScheme
|
|
{
|
|
/* WARNING: The numerical value defines the preference when multiple
|
|
* challenges are provided by the server.
|
|
* The lower the numerical value, the lesser the preference.
|
|
* So give stronger methods higher values.
|
|
* See strengthGreater()
|
|
*/
|
|
BasicAuthenticationScheme = 100, //!< HTTP Basic authentication according to RFC 7617
|
|
UnknownAuthenticationScheme = -1 //!< Unknown authentication scheme
|
|
};
|
|
|
|
/*! Creates an invalid authentication challenge.
|
|
*
|
|
* Sets the behaviorFlags() to Behavior_Expected.
|
|
*/
|
|
Challenge()
|
|
{
|
|
setBehaviorFlags(Behavior_Expected);
|
|
}
|
|
/*! Enforces a virtual destructor.
|
|
*/
|
|
virtual ~Challenge()
|
|
{
|
|
}
|
|
|
|
/*! \return The authentication scheme of this Challenge.
|
|
*/
|
|
virtual AuthenticationScheme scheme() const = 0;
|
|
|
|
/*! Checks if the Challenge is valid, meaning it contains all
|
|
* parameters required for the authentication scheme.
|
|
*
|
|
* \return \c true if the Challenge is valid.
|
|
*/
|
|
virtual bool isValid() const = 0;
|
|
/*! \return The parameters of the Challenge as a QVariantMap.
|
|
*/
|
|
virtual QVariantMap parameters() const = 0;
|
|
/*! \return The value of an Authenticate header representing this Challenge.
|
|
*/
|
|
virtual QByteArray authenticateHeader() const = 0;
|
|
/*! Compares the cryptographic strength of this Challenge with another
|
|
* Challenge.
|
|
*
|
|
* \param other The Challenge to compare against.
|
|
* \return \c true if this Challenge is considered cryptographically
|
|
* stronger than \p other. If they are equal or if \p other is stronger,
|
|
* \c false is returned.
|
|
*/
|
|
virtual bool strengthGreater(const Challenge::Ptr& other)
|
|
{
|
|
return this->scheme() > other->scheme();
|
|
}
|
|
|
|
/*! Tunes the behavior of this Challenge.
|
|
*
|
|
* \param behaviorFlags Combination of BehaviorFlags to define some details of this Challenge's
|
|
* behavior. \note Only certain BehaviorFlags have an effect on a Challenge. \sa BehaviorFlag
|
|
*/
|
|
void setBehaviorFlags(BehaviorFlags behaviorFlags)
|
|
{
|
|
m_behaviorFlags = behaviorFlags;
|
|
}
|
|
|
|
/*! \return The BehaviorFlags currently active for this Challenge.
|
|
*/
|
|
BehaviorFlags behaviorFlags() const
|
|
{
|
|
return m_behaviorFlags;
|
|
}
|
|
|
|
/*! \return The realm of this Challenge according to RFC 7235 2.2.
|
|
*/
|
|
QString realm() const
|
|
{
|
|
return m_realm;
|
|
}
|
|
|
|
/*! \return The name of the realm parameter. Also used as key in QVariantMaps.
|
|
*/
|
|
static QString realmKey()
|
|
{
|
|
return QStringLiteral("realm");
|
|
}
|
|
|
|
/*! Adds an authorization header for this Challenge to a given request.
|
|
*
|
|
* \param request The QNetworkRequest to which the authorization header will be added.
|
|
* \param operation The HTTP verb.
|
|
* \param body The message body of the request.
|
|
* \param authenticator The QAuthenticator providing the credentials to be used for the
|
|
* authorization.
|
|
*/
|
|
void addAuthorization(QNetworkRequest& request,
|
|
QNetworkAccessManager::Operation operation,
|
|
const QByteArray& body,
|
|
const QAuthenticator& authenticator)
|
|
{
|
|
const QByteArray authHeaderValue =
|
|
authorizationHeaderValue(request, operation, body, authenticator);
|
|
request.setRawHeader(detail::authorizationHeaderKey(), authHeaderValue);
|
|
}
|
|
|
|
/*! Verifies if a given request contains a valid authorization for this Challenge.
|
|
*
|
|
* \param request The request which requests authorization.
|
|
* \param authenticator The QAuthenticator providing a set of valid credentials.
|
|
* \return \c true if the \p request contains a valid authorization header matching
|
|
* this Challenge and the credentials provided by the \p authenticator.
|
|
* Note that for certain authentication schemes, this method might always return \c false if this
|
|
* Challenge is invalid (see isValid()).
|
|
*/
|
|
virtual bool verifyAuthorization(const QNetworkRequest& request,
|
|
const QAuthenticator& authenticator) = 0;
|
|
|
|
/*! Implements a "lesser" comparison based on the cryptographic strength of a Challenge.
|
|
*/
|
|
struct StrengthCompare
|
|
{
|
|
/*! Implements the lesser comparison.
|
|
*
|
|
* \param left The left-hand side Challenge of the comparison.
|
|
* \param right The right-hand side Challenge of the comparison.
|
|
* \return \c true if \p left < \p right regarding the strength of the algorithm
|
|
* used by the challenges. Otherwise \c false.
|
|
*/
|
|
bool operator()(const Challenge::Ptr& left, const Challenge::Ptr& right) const
|
|
{
|
|
return right->strengthGreater(left);
|
|
}
|
|
};
|
|
|
|
protected:
|
|
/*! Generates a new authorization header value for this Challenge.
|
|
*
|
|
* \note This method is non-const because an authentication scheme might need
|
|
* to remember parameters from the authorizations it gave (like the \c cnonce in the Digest scheme).
|
|
*
|
|
* \param request The request for with the authorization header should be generated.
|
|
* \param operation The HTTP verb of the request.
|
|
* \param body The message body of the request.
|
|
* \param authenticator The QAuthenticator providing the credentials to be used to generate the
|
|
* authorization header.
|
|
* \return The value of the Authorization header to request authorization for this Challenge using the
|
|
* credentials provided by the \p authenticator.
|
|
*
|
|
* \sa addAuthorization()
|
|
*/
|
|
virtual QByteArray authorizationHeaderValue(const QNetworkRequest& request,
|
|
QNetworkAccessManager::Operation operation,
|
|
const QByteArray& body,
|
|
const QAuthenticator& authenticator) = 0;
|
|
|
|
/*! Splits a list of authentication parameters according to RFC 7235 2.1. into a QVariantMap.
|
|
*
|
|
* \param authParams The list of name=value strings.
|
|
* \param[out] paramsValid If not \c NULL, the value of this boolean will be set to \c false if one of
|
|
* the parameters in \p authParams was malformed or to \c true otherwise. If \p paramsValid is \c NULL,
|
|
* it is ignored. \return A QVariantMap mapping the names of the authentication parameters to their
|
|
* values. The names of the authentication parameters are converted to lower case. The values are *not*
|
|
* unquoted in case they are quoted strings.
|
|
*/
|
|
static QVariantMap stringParamListToMap(const QStringList& authParams, bool* paramsValid = Q_NULLPTR)
|
|
{
|
|
QVariantMap result;
|
|
|
|
QStringList::const_iterator paramIter = authParams.cbegin();
|
|
const QStringList::const_iterator paramsEnd = authParams.cend();
|
|
const QRegularExpression authParamRegEx(detail::authParamPattern());
|
|
|
|
for (; paramIter != paramsEnd; ++paramIter) {
|
|
const QRegularExpressionMatch authParamMatch = authParamRegEx.match(*paramIter);
|
|
if (!authParamMatch.hasMatch()) {
|
|
qCWarning(log) << "Invalid authentication header: malformed auth-param:" << *paramIter;
|
|
if (paramsValid)
|
|
*paramsValid = false;
|
|
return QVariantMap();
|
|
}
|
|
const QString authParamName =
|
|
authParamMatch.captured(QStringLiteral("authParamName")).toLower();
|
|
const QString authParamValue = authParamMatch.captured(QStringLiteral("authParamValue"));
|
|
|
|
if (result.contains(authParamName))
|
|
qCWarning(log) << "Invalid authentication header: auth-param occurred multiple times:"
|
|
<< authParamName;
|
|
|
|
result.insert(authParamName, authParamValue);
|
|
}
|
|
|
|
if (paramsValid)
|
|
*paramsValid = true;
|
|
return result;
|
|
}
|
|
|
|
/*! Sets the realm of this Challenge.
|
|
*
|
|
* The base class does not use the realm. It just provides the property for convenience.
|
|
* So derived classes are free to use the realm as they need to.
|
|
*
|
|
* \param realm The realm.
|
|
* \sa realm()
|
|
*/
|
|
void setRealm(const QString& realm)
|
|
{
|
|
m_realm = realm;
|
|
}
|
|
|
|
private:
|
|
QString m_realm;
|
|
BehaviorFlags m_behaviorFlags;
|
|
};
|
|
|
|
/*! HTTP Basic authentication scheme according to RFC 7617.
|
|
*
|
|
* \sa https://tools.ietf.org/html/rfc7617
|
|
*/
|
|
class Basic : public Challenge
|
|
{
|
|
public:
|
|
/*! Creates a Basic authentication Challenge with parameters as a QStringList.
|
|
*
|
|
* \param authParams The parameters of the challenge as a list of name=value strings.
|
|
*/
|
|
explicit Basic(const QStringList& authParams)
|
|
: Challenge()
|
|
{
|
|
bool paramsValid = false;
|
|
const QVariantMap authParamsMap = stringParamListToMap(authParams, ¶msValid);
|
|
if (paramsValid)
|
|
readParameters(authParamsMap);
|
|
}
|
|
|
|
/*! Creates a Basic authentication Challenge with parameters a QVariantMap.
|
|
*
|
|
* \param authParams The parameters of the challenge as a map.
|
|
*/
|
|
explicit Basic(const QVariantMap& authParams)
|
|
: Challenge()
|
|
{
|
|
readParameters(authParams);
|
|
}
|
|
|
|
/*! Creates a Basic authentication Challenge with the given realm.
|
|
*
|
|
* \param realm The realm.
|
|
*/
|
|
explicit Basic(const QString& realm)
|
|
: Challenge()
|
|
{
|
|
QVariantMap params;
|
|
params.insert(realmKey(), realm);
|
|
readParameters(params);
|
|
}
|
|
|
|
/*! \return Challenge::BasicAuthenticationScheme.
|
|
*/
|
|
virtual AuthenticationScheme scheme() const Q_DECL_OVERRIDE
|
|
{
|
|
return BasicAuthenticationScheme;
|
|
}
|
|
|
|
/*! \return The identifier string of the Basic authentication scheme.
|
|
*/
|
|
static QByteArray schemeString()
|
|
{
|
|
return "Basic";
|
|
}
|
|
/*! \return The name of the charset parameter. Also used as key in QVariantMaps.
|
|
*/
|
|
static QString charsetKey()
|
|
{
|
|
return QStringLiteral("charset");
|
|
}
|
|
|
|
/*! \return \c true if the realm parameter is defined. Note that the realm can still
|
|
* be empty (`""`).
|
|
*/
|
|
virtual bool isValid() const Q_DECL_OVERRIDE
|
|
{
|
|
return !realm().isNull();
|
|
}
|
|
|
|
/*! \return A map containing the realm and charset parameters (if given).
|
|
* \sa realmKey(), charsetKey().
|
|
*/
|
|
virtual QVariantMap parameters() const Q_DECL_OVERRIDE
|
|
{
|
|
QVariantMap params;
|
|
params[realmKey()] = realm();
|
|
if (!m_charset.isEmpty())
|
|
params[charsetKey()] = m_charset;
|
|
return params;
|
|
}
|
|
/*! \copydoc Challenge::authenticateHeader()
|
|
*/
|
|
virtual QByteArray authenticateHeader() const Q_DECL_OVERRIDE
|
|
{
|
|
if (!isValid())
|
|
return QByteArray();
|
|
|
|
QByteArray result =
|
|
schemeString() + " " + realmKey().toLatin1() + "=" + quoteString(realm()).toLatin1();
|
|
if (!m_charset.isEmpty())
|
|
result += ", " + charsetKey().toLatin1() + "=" + quoteString(m_charset).toLatin1();
|
|
return result;
|
|
}
|
|
|
|
/*! \copydoc Challenge::verifyAuthorization(const QNetworkRequest&, const QAuthenticator&)
|
|
*/
|
|
virtual bool verifyAuthorization(const QNetworkRequest& request,
|
|
const QAuthenticator& authenticator) Q_DECL_OVERRIDE
|
|
{
|
|
/* Since the authorization header of the Basic scheme is very simple, we can simply compare
|
|
* the textual representations.
|
|
* Additionally, we can verify the authorization even if this challenge is invalid.
|
|
*/
|
|
const QByteArray reqAuth = request.rawHeader(detail::authorizationHeaderKey());
|
|
const QByteArray challengeAuth = this->authorizationHeaderValue(
|
|
QNetworkRequest(), QNetworkAccessManager::GetOperation, QByteArray(), authenticator);
|
|
return reqAuth == challengeAuth;
|
|
}
|
|
|
|
protected:
|
|
/*! \copydoc Challenge::authorizationHeaderValue()
|
|
*/
|
|
virtual QByteArray authorizationHeaderValue(const QNetworkRequest& request,
|
|
QNetworkAccessManager::Operation operation,
|
|
const QByteArray& body,
|
|
const QAuthenticator& authenticator) Q_DECL_OVERRIDE
|
|
{
|
|
Q_UNUSED(request)
|
|
Q_UNUSED(operation)
|
|
Q_UNUSED(body)
|
|
|
|
QByteArray userName;
|
|
QByteArray password;
|
|
if (behaviorFlags().testFlag(Behavior_HttpAuthLatin1Encoding)) {
|
|
userName = authenticator.user().toLatin1();
|
|
password = authenticator.password().toLatin1();
|
|
} else {
|
|
/* No need to check m_charset since UTF-8 is the only allowed encoding at the moment and
|
|
* we use UTF-8 by default anyway (so even if charset was not specified)
|
|
*/
|
|
userName = authenticator.user().normalized(QString::NormalizationForm_C).toUtf8();
|
|
password = authenticator.password().normalized(QString::NormalizationForm_C).toUtf8();
|
|
}
|
|
|
|
return schemeString() + " " + (userName + ":" + password).toBase64();
|
|
}
|
|
|
|
private:
|
|
void readParameters(const QVariantMap& params)
|
|
{
|
|
if (!params.contains(realmKey())) {
|
|
setRealm(QString());
|
|
qCWarning(log) << "Invalid authentication header: Missing required parameter: \"realm\"";
|
|
return;
|
|
}
|
|
|
|
// Realm
|
|
const QString realmValue = params.value(realmKey()).toString();
|
|
const QString realm =
|
|
HttpUtils::isValidToken(realmValue) ? realmValue : HttpUtils::unquoteString(realmValue);
|
|
if (realm.isNull()) {
|
|
qCWarning(log) << "Invalid authentication header: Missing value for parameter: \"realm\"";
|
|
return;
|
|
}
|
|
setRealm(realm);
|
|
|
|
// Charset
|
|
if (params.contains(charsetKey())) {
|
|
const QString charsetValue = params.value(charsetKey()).toString();
|
|
const QString charset =
|
|
(HttpUtils::isValidToken(charsetValue) ? charsetValue
|
|
: HttpUtils::unquoteString(charsetValue))
|
|
.toLower();
|
|
m_charset = charset;
|
|
}
|
|
}
|
|
|
|
QString m_charset;
|
|
};
|
|
|
|
/*! \internal Implementation details
|
|
*/
|
|
namespace detail
|
|
{
|
|
inline Challenge::Ptr parseAuthenticateChallenge(const QStringList& challengeParts, const QUrl&)
|
|
{
|
|
const QString& challengeStart = challengeParts.at(0);
|
|
const int schemeSeparatorIndex = challengeStart.indexOf(QChar::fromLatin1(' '));
|
|
const QString authSchemeLower =
|
|
HttpUtils::trimmed(challengeStart.left(schemeSeparatorIndex)).toLower();
|
|
const QString firstAuthParam =
|
|
(schemeSeparatorIndex > 0) ? HttpUtils::trimmed(challengeStart.mid(schemeSeparatorIndex + 1))
|
|
: QString();
|
|
|
|
// Get the first parameter of the challenge
|
|
QStringList authParams;
|
|
if (!firstAuthParam.isEmpty())
|
|
authParams << firstAuthParam;
|
|
// Append further parameters of the challenge
|
|
if (challengeParts.size() > 1)
|
|
authParams << challengeParts.mid(1);
|
|
|
|
const QString basicAuthSchemeLower = QString::fromLatin1(Basic::schemeString()).toLower();
|
|
if (authSchemeLower == basicAuthSchemeLower)
|
|
return Challenge::Ptr(new Basic(authParams));
|
|
|
|
qCWarning(log) << "Unsupported authentication scheme:" << authSchemeLower;
|
|
return Challenge::Ptr();
|
|
}
|
|
|
|
inline QVector<QStringList> splitAuthenticateHeaderIntoChallengeParts(const QString& headerValue)
|
|
{
|
|
QVector<QStringList> result;
|
|
|
|
const QStringList headerSplit = HttpUtils::splitCommaSeparatedList(headerValue);
|
|
|
|
const QRegularExpression challengeStartRegEx(
|
|
QStringLiteral("^") + HttpUtils::tokenPattern() + QStringLiteral("(?:")
|
|
+ HttpUtils::lwsPattern() + QStringLiteral("(?:") + HttpUtils::token68Pattern()
|
|
+ QStringLiteral("|") + detail::authParamPattern() + QStringLiteral("))?"));
|
|
|
|
QVector<QPair<int, int>> challengeIndexes;
|
|
int challengeStartIndex = headerSplit.indexOf(challengeStartRegEx);
|
|
if (challengeStartIndex < 0) {
|
|
qCWarning(log) << "Invalid authentication header: expected start of authentication challenge";
|
|
return result;
|
|
}
|
|
while (challengeStartIndex != -1) {
|
|
const int nextChallengeStartIndex =
|
|
headerSplit.indexOf(challengeStartRegEx, challengeStartIndex + 1);
|
|
challengeIndexes << ::qMakePair(challengeStartIndex, nextChallengeStartIndex);
|
|
challengeStartIndex = nextChallengeStartIndex;
|
|
}
|
|
|
|
QVector<QPair<int, int>>::const_iterator challengeIndexIter = challengeIndexes.cbegin();
|
|
const QVector<QPair<int, int>>::const_iterator challengeIndexesEnd = challengeIndexes.cend();
|
|
|
|
for (; challengeIndexIter != challengeIndexesEnd; ++challengeIndexIter) {
|
|
const int challengePartCount = (challengeIndexIter->second == -1)
|
|
? (headerSplit.size() - challengeIndexIter->first)
|
|
: (challengeIndexIter->second - challengeIndexIter->first);
|
|
const QStringList challengeParts =
|
|
headerSplit.mid(challengeIndexIter->first, challengePartCount);
|
|
|
|
result << challengeParts;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
inline QVector<Challenge::Ptr> parseAuthenticateHeader(const QString& headerValue,
|
|
const QUrl& requestingUrl)
|
|
{
|
|
QVector<Challenge::Ptr> result;
|
|
|
|
const QVector<QStringList> challenges = splitAuthenticateHeaderIntoChallengeParts(headerValue);
|
|
|
|
QVector<QStringList>::const_iterator challengeIter = challenges.cbegin();
|
|
const QVector<QStringList>::const_iterator challengesEnd = challenges.cend();
|
|
|
|
for (; challengeIter != challengesEnd; ++challengeIter) {
|
|
const Challenge::Ptr authChallenge = parseAuthenticateChallenge(*challengeIter, requestingUrl);
|
|
|
|
if (authChallenge && authChallenge->isValid())
|
|
result << authChallenge;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
inline QVector<Challenge::Ptr> parseAuthenticateHeaders(const QNetworkReply* reply)
|
|
{
|
|
const QByteArray wwwAuthenticateHeaderLower = wwwAuthenticateHeader().toLower();
|
|
QVector<Challenge::Ptr> authChallenges;
|
|
const QUrl requestingUrl = reply->url();
|
|
|
|
const QList<QByteArray> rawHeaderList = reply->rawHeaderList();
|
|
QList<QByteArray>::const_iterator headerIter = rawHeaderList.cbegin();
|
|
const QList<QByteArray>::const_iterator headerEnd = rawHeaderList.cend();
|
|
for (; headerIter != headerEnd; ++headerIter) {
|
|
if (headerIter->toLower() == wwwAuthenticateHeaderLower) {
|
|
const QString headerValue =
|
|
HttpUtils::whiteSpaceCleaned(QString::fromLatin1(reply->rawHeader(*headerIter)));
|
|
if (headerValue.isEmpty())
|
|
continue;
|
|
|
|
authChallenges << parseAuthenticateHeader(headerValue, requestingUrl);
|
|
}
|
|
}
|
|
return authChallenges;
|
|
}
|
|
|
|
} // namespace detail
|
|
|
|
/*! Extracts all authentication challenges from a QNetworkReply.
|
|
*
|
|
* \param reply The reply object potentially containing authentication challenges.
|
|
* \return A vector of Challenge::Ptrs. The vector can be empty if \p reply did not
|
|
* contain any authentication challenges.
|
|
*/
|
|
inline QVector<Challenge::Ptr> getAuthenticationChallenges(const QNetworkReply* reply)
|
|
{
|
|
const HttpStatus::Code statusCode =
|
|
static_cast<HttpStatus::Code>(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
|
|
switch (statusCode) {
|
|
case HttpStatus::Unauthorized:
|
|
return detail::parseAuthenticateHeaders(reply);
|
|
|
|
case HttpStatus::ProxyAuthenticationRequired:
|
|
// TODO: Implement proxy authentication
|
|
qCWarning(log) << "Proxy authentication is not supported at the moment";
|
|
break;
|
|
|
|
// LCOV_EXCL_START
|
|
default:
|
|
Q_ASSERT_X(false,
|
|
Q_FUNC_INFO,
|
|
"MockNetworkAccessManager: Internal error: trying to authenticate"
|
|
"request which doesn't require authentication");
|
|
break;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
|
|
return QVector<Challenge::Ptr>();
|
|
}
|
|
|
|
} // namespace Authentication
|
|
|
|
} // namespace HttpUtils
|
|
|
|
/*! Provides helper methods for tasks related to FTP.
|
|
*
|
|
* \since 0.6.0
|
|
*/
|
|
namespace FtpUtils
|
|
{
|
|
|
|
/*! The default port of FTP requests.
|
|
*/
|
|
const int FtpDefaultPort = 21;
|
|
|
|
/*! \return The scheme of the File Transfer Protocol (FTP) in lower case characters.
|
|
* \since 0.6.0
|
|
*/
|
|
inline QString ftpScheme()
|
|
{
|
|
const QString ftpSchemeString = QStringLiteral("ftp");
|
|
return ftpSchemeString;
|
|
}
|
|
|
|
/*! \return The scheme of the File Transfer Protocol over SSL (FTPS) in lower case characters.
|
|
* \since 0.6.0
|
|
*/
|
|
inline QString ftpsScheme()
|
|
{
|
|
const QString ftpsSchemeString = QStringLiteral("ftps");
|
|
return ftpsSchemeString;
|
|
}
|
|
|
|
} // namespace FtpUtils
|
|
|
|
/*! Provides helper methods for tasks related to data: URLs.
|
|
*
|
|
* \since 0.9.0
|
|
*/
|
|
namespace DataUrlUtils
|
|
{
|
|
/*! \return The scheme of data: URLs in lower case characters.
|
|
* \since 0.9.0
|
|
*/
|
|
inline QString dataScheme()
|
|
{
|
|
const QString dataSchemeString = QStringLiteral("data");
|
|
return dataSchemeString;
|
|
}
|
|
} // namespace DataUrlUtils
|
|
|
|
/*! Provides helper methods for tasks related to file: and qrc: URLs.
|
|
*
|
|
* \since 0.9.0
|
|
*/
|
|
namespace FileUtils
|
|
{
|
|
/*! \return The scheme of file: URLs in lower case characters.
|
|
* \since 0.9.0
|
|
*/
|
|
inline QString fileScheme()
|
|
{
|
|
const QString fileSchemeString = QStringLiteral("file");
|
|
return fileSchemeString;
|
|
}
|
|
|
|
/*! \return The scheme of qrc: URLs in lower case characters.
|
|
* \since 0.9.0
|
|
*/
|
|
inline QString qrcScheme()
|
|
{
|
|
const QString qrcSchemeString = QStringLiteral("qrc");
|
|
return qrcSchemeString;
|
|
}
|
|
|
|
#if defined(Q_OS_ANDROID)
|
|
inline QString assetsScheme()
|
|
{
|
|
const QString assetsSchemeString = QStringLiteral("assets");
|
|
return assetsSchemeString;
|
|
}
|
|
#endif
|
|
|
|
/*! Checks if a scheme behaves like the file scheme.
|
|
* \param scheme The scheme to be checked to behave like the file scheme.
|
|
* \return \c true if the \p url has a `file:`, `qrc:` or on Android `assets:` scheme. \c false otherwise.
|
|
*/
|
|
inline bool isFileLikeScheme(const QString& scheme)
|
|
{
|
|
#if defined(Q_OS_ANDROID)
|
|
if (scheme == assetsScheme())
|
|
return true;
|
|
#endif
|
|
return scheme == fileScheme() || scheme == qrcScheme();
|
|
}
|
|
|
|
/*! Checks if a URL has a file-like scheme.
|
|
* \param url The URL to be checked for a file-like scheme.
|
|
* \return \c true if the \p url has a file: or qrc: scheme. \c false otherwise.
|
|
*/
|
|
inline bool isFileLikeScheme(const QUrl& url)
|
|
{
|
|
return isFileLikeScheme(url.scheme());
|
|
}
|
|
|
|
} // namespace FileUtils
|
|
|
|
/*! Represents a version number.
|
|
* A version number is a sequence of (dot separated) unsigned integers potentially followed by a suffix.
|
|
*
|
|
* \since 0.3.0
|
|
*/
|
|
struct VersionNumber
|
|
{
|
|
/*! The container type holding the version segments.
|
|
* \sa segments
|
|
*/
|
|
typedef std::vector<unsigned int> SegmentVector;
|
|
|
|
/*! The numeric segments that make up the version number.
|
|
*/
|
|
SegmentVector segments;
|
|
/*! The non-numeric suffix of the version number.
|
|
*/
|
|
QString suffix;
|
|
|
|
/*! \return `"."` which is the string separating the version segments in the string representation of the
|
|
* version number.
|
|
*/
|
|
static QString segmentSeparator()
|
|
{
|
|
const QString separator = QStringLiteral(".");
|
|
return separator;
|
|
}
|
|
|
|
/*! Creates an empty VersionNumber.
|
|
*/
|
|
VersionNumber()
|
|
{
|
|
}
|
|
|
|
/*! Creates a VersionNumber from three segments.
|
|
* \param major The major version number.
|
|
* \param minor The minor version number.
|
|
* \param patch The patch version number.
|
|
* \param suffix An optional version suffix.
|
|
*/
|
|
explicit VersionNumber(unsigned int major,
|
|
unsigned int minor,
|
|
unsigned int patch,
|
|
const QString& suffix = QString())
|
|
{
|
|
segments.push_back(major);
|
|
segments.push_back(minor);
|
|
segments.push_back(patch);
|
|
this->suffix = suffix;
|
|
}
|
|
|
|
/*! Creates a VersionNumber from a string representation.
|
|
* \param versionStr The string representing the version number.
|
|
* \return A VersionNumber object corresponding to the \p versionStr or an
|
|
* empty VersionNumber object if the \p versionStr could not be parsed.
|
|
*/
|
|
static VersionNumber fromString(const QString& versionStr)
|
|
{
|
|
VersionNumber version;
|
|
const QStringList split = versionStr.split(segmentSeparator());
|
|
|
|
version.segments.reserve(static_cast<SegmentVector::size_type>(split.size()));
|
|
|
|
bool converted = true;
|
|
QStringList::const_iterator iter = split.cbegin();
|
|
const QStringList::const_iterator splitEnd = split.cend();
|
|
for (; iter != splitEnd; ++iter) {
|
|
const unsigned int number = iter->toUInt(&converted);
|
|
if (!converted)
|
|
break;
|
|
version.segments.push_back(number);
|
|
}
|
|
|
|
if (!converted) {
|
|
// There is a suffix
|
|
const QString lastSegment = *iter;
|
|
const QRegularExpression digitRegEx(QStringLiteral("^\\d+"));
|
|
const QRegularExpressionMatch match = digitRegEx.match(lastSegment);
|
|
if (match.hasMatch())
|
|
version.segments.push_back(match.captured().toUInt());
|
|
version.suffix = lastSegment.mid(match.capturedLength());
|
|
}
|
|
|
|
return version;
|
|
}
|
|
|
|
/*! \return The string representation of this version number.
|
|
*/
|
|
QString toString() const
|
|
{
|
|
QString result;
|
|
const SegmentVector& segs = segments;
|
|
SegmentVector::const_iterator segIter = segs.begin();
|
|
const SegmentVector::const_iterator segsEnd = segs.end();
|
|
for (; segIter != segsEnd; ++segIter)
|
|
result += QString::number(*segIter) + segmentSeparator();
|
|
result.chop(segmentSeparator().size());
|
|
result += suffix;
|
|
return result;
|
|
}
|
|
|
|
/*! Compares two VersionNumbers for equality.
|
|
* \param left One VersionNumber.
|
|
* \param right Another VersionNumber.
|
|
* \return \c true if \p left and \p right represent the same version number.
|
|
* \note Missing parts in a VersionNumber are interpreted as 0.
|
|
*/
|
|
friend bool operator==(const VersionNumber& left, const VersionNumber& right)
|
|
{
|
|
if (&left == &right)
|
|
return true;
|
|
|
|
SegmentVector leftSegments = left.segments;
|
|
SegmentVector rightSegments = right.segments;
|
|
|
|
const SegmentVector::size_type maxSize = std::max(leftSegments.size(), rightSegments.size());
|
|
leftSegments.resize(maxSize);
|
|
rightSegments.resize(maxSize);
|
|
|
|
return leftSegments == rightSegments && left.suffix == right.suffix;
|
|
}
|
|
|
|
/*! Compares two VersionNumbers for inequality.
|
|
* \param left One VersionNumber.
|
|
* \param right Another VersionNumber.
|
|
* \return \c true if \p left and \p right represent different version numbers.
|
|
* \note Missing parts in a VersionNumber are interpreted as 0.
|
|
*/
|
|
friend bool operator!=(const VersionNumber& left, const VersionNumber& right)
|
|
{
|
|
return !(left == right);
|
|
}
|
|
|
|
/*! Compares if a VersionNumber is lesser than another VersionNumber.
|
|
* \param left One VersionNumber.
|
|
* \param right Another VersionNumber.
|
|
* \return \c true if \p left is lesser than \p right.
|
|
* \note Missing parts in a VersionNumber are interpreted as 0.
|
|
*/
|
|
friend bool operator<(const VersionNumber& left, const VersionNumber& right)
|
|
{
|
|
std::vector<unsigned int>::const_iterator leftIter = left.segments.begin();
|
|
const std::vector<unsigned int>::const_iterator leftEnd = left.segments.end();
|
|
std::vector<unsigned int>::const_iterator rightIter = right.segments.begin();
|
|
const std::vector<unsigned int>::const_iterator rightEnd = right.segments.end();
|
|
|
|
while (leftIter != leftEnd || rightIter != rightEnd) {
|
|
const unsigned int leftPart = (leftIter != leftEnd) ? *leftIter : 0;
|
|
const unsigned int rightPart = (rightIter != rightEnd) ? *rightIter : 0;
|
|
|
|
if (leftPart < rightPart)
|
|
return true;
|
|
if (leftPart > rightPart)
|
|
return false;
|
|
|
|
if (leftIter != leftEnd)
|
|
++leftIter;
|
|
if (rightIter != rightEnd)
|
|
++rightIter;
|
|
}
|
|
|
|
if (left.suffix.isEmpty() && !right.suffix.isEmpty())
|
|
return false;
|
|
if (!left.suffix.isEmpty() && right.suffix.isEmpty())
|
|
return true;
|
|
return left.suffix < right.suffix;
|
|
}
|
|
|
|
/*! Compares if a VersionNumber is greater than another VersionNumber.
|
|
* \param left One VersionNumber.
|
|
* \param right Another VersionNumber.
|
|
* \return \c true if \p left is greater than \p right.
|
|
* \note Missing parts in a VersionNumber are interpreted as 0.
|
|
*/
|
|
friend bool operator>(const VersionNumber& left, const VersionNumber& right)
|
|
{
|
|
std::vector<unsigned int>::const_iterator leftIter = left.segments.begin();
|
|
const std::vector<unsigned int>::const_iterator leftEnd = left.segments.end();
|
|
std::vector<unsigned int>::const_iterator rightIter = right.segments.begin();
|
|
const std::vector<unsigned int>::const_iterator rightEnd = right.segments.end();
|
|
|
|
while (leftIter != leftEnd || rightIter != rightEnd) {
|
|
const unsigned int leftPart = (leftIter != leftEnd) ? *leftIter : 0;
|
|
const unsigned int rightPart = (rightIter != rightEnd) ? *rightIter : 0;
|
|
|
|
if (leftPart > rightPart)
|
|
return true;
|
|
if (leftPart < rightPart)
|
|
return false;
|
|
|
|
if (leftIter != leftEnd)
|
|
++leftIter;
|
|
if (rightIter != rightEnd)
|
|
++rightIter;
|
|
}
|
|
|
|
if (left.suffix.isEmpty() && !right.suffix.isEmpty())
|
|
return true;
|
|
if (!left.suffix.isEmpty() && right.suffix.isEmpty())
|
|
return false;
|
|
return left.suffix > right.suffix;
|
|
}
|
|
|
|
/*! Compares if a VersionNumber is greater than or equal to another VersionNumber.
|
|
* \param left One VersionNumber.
|
|
* \param right Another VersionNumber.
|
|
* \return \c true if \p left is greater than or equal to \p right.
|
|
* \note Missing parts in a VersionNumber are interpreted as 0.
|
|
*/
|
|
friend bool operator>=(const VersionNumber& left, const VersionNumber& right)
|
|
{
|
|
return !(left < right);
|
|
}
|
|
|
|
/*! Compares if a VersionNumber is lesser than or equal to another VersionNumber.
|
|
* \param left One VersionNumber.
|
|
* \param right Another VersionNumber.
|
|
* \return \c true if \p left is lesser than or equal to \p right.
|
|
* \note Missing parts in a VersionNumber are interpreted as 0.
|
|
*/
|
|
friend bool operator<=(const VersionNumber& left, const VersionNumber& right)
|
|
{
|
|
return !(left > right);
|
|
}
|
|
};
|
|
|
|
/*! Wrapper class providing a common interface for string/text decoding.
|
|
*
|
|
* This class is an implementation of the bridge pattern combined with the adapter
|
|
* pattern (wrapper). Its implementation is either realized by a QTextCodec or
|
|
* by a QStringDecoder. Which implementation is used depends on the availability.
|
|
* If both are available, QTextCodec is used unless the StringDecoder is
|
|
* constructed with a QStringDecoder.
|
|
*
|
|
* This class mainly exists to provide compatibility for both %Qt 5 and %Qt 6 since
|
|
* the QTextCodec class was deprecated in %Qt 6.
|
|
*
|
|
* \warning A StringDecoder must be valid to be used. Trying to decode with an invalid
|
|
* decoder might result in undefined behavior. See isValid().
|
|
*
|
|
* \since 0.5.0
|
|
*/
|
|
class StringDecoder
|
|
{
|
|
public:
|
|
/*! Creates a StringDecoder with an optional codec.
|
|
*
|
|
* \param codec The name of the codec which this StringDecoder should decode.
|
|
* If \p codec is empty or unknown to the implementation, the StringDecoder will
|
|
* be invalid.
|
|
*
|
|
* \sa isValid()
|
|
* \sa setCodec()
|
|
*/
|
|
explicit StringDecoder(const QString& codec = QString())
|
|
{
|
|
if (!codec.isEmpty())
|
|
setCodec(codec);
|
|
}
|
|
|
|
#if defined(MOCKNETWORKACCESSMANAGER_QT_HAS_TEXTCODEC)
|
|
/*! Creates a StringDecoder which uses the given QTextCodec as implementation.
|
|
*
|
|
* \param codec The QTextCodec to be used to decode the data.
|
|
* If \p codec is `NULL`, the constructed StringDecoder will be invalid.
|
|
*/
|
|
StringDecoder(QTextCodec* codec);
|
|
#endif
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
/*! Creates a StringDecoder which uses the given QStringDecoder as implementation.
|
|
*
|
|
* \note Since StringDecoder is stateless, it will call QStringDecoder::resetState()
|
|
* on the \p decoder every time before it decodes data.
|
|
*
|
|
* \param decoder The QStringDecoder to be used to decode the data. If \p decoder
|
|
* contains a `nullptr`, the constructed StringDecoder will be invalid.
|
|
*/
|
|
StringDecoder(std::unique_ptr<QStringDecoder>&& decoder);
|
|
#endif
|
|
|
|
/*! Creates a copy of another StringDecoder.
|
|
*
|
|
* The constructed StringDecoder will use the same implementation
|
|
* as \p other.
|
|
*
|
|
* \param other The StringDecoder to be copied.
|
|
*/
|
|
StringDecoder(const StringDecoder& other)
|
|
{
|
|
if (other.m_impl)
|
|
m_impl.reset(other.m_impl->clone());
|
|
}
|
|
|
|
/*! Creates a StringDecoder by moving another one.
|
|
*
|
|
* \param other The StringDecoder to be moved.
|
|
*/
|
|
StringDecoder(StringDecoder&& other) = default;
|
|
|
|
/*! Destroys this StringDecoder and its implementation.
|
|
*/
|
|
~StringDecoder()
|
|
{
|
|
// unique_ptr takes care of clean up
|
|
// This destructor just exists to fix SonarCloud cpp:S3624
|
|
}
|
|
|
|
/*! Makes this StringDecoder use the same implementation as another one.
|
|
*
|
|
* \param other The StringDecoder whose implementation is copied.
|
|
* \return A reference to this StringDecoder.
|
|
*/
|
|
StringDecoder& operator=(StringDecoder other)
|
|
{
|
|
m_impl.swap(other.m_impl);
|
|
return *this;
|
|
}
|
|
|
|
/*! Makes this StringDecoder use the implementation of another one.
|
|
*
|
|
* \param other The StringDecoder whose implementation is moved.
|
|
* \return A reference to this StringDecoder.
|
|
*/
|
|
StringDecoder& operator=(StringDecoder&& other) = default;
|
|
|
|
/*! Checks if this StringDecoder can decode data.
|
|
*
|
|
* Trying to decode data with an invalid StringDecoder may result in undefined
|
|
* behavior.
|
|
*
|
|
* \return \c true if this StringDecoder contains a valid implementation
|
|
* and can decode data.
|
|
*/
|
|
bool isValid() const
|
|
{
|
|
return m_impl && m_impl->isValid();
|
|
}
|
|
|
|
/*! Sets the codec used by this StringDecoder.
|
|
*
|
|
* \param codec The name of the codec to be used to decode data.
|
|
* If \p codec is empty or unknown to the implementation, this StringDecoder
|
|
* becomes invalid.
|
|
*
|
|
* \sa isValid()
|
|
*/
|
|
void setCodec(const QString& codec)
|
|
{
|
|
ensureImpl();
|
|
m_impl->setCodec(codec);
|
|
}
|
|
|
|
/*! Sets the codec by trying to detect the codec of given data.
|
|
*
|
|
* If the codec cannot be detected and \p fallbackCodec is empty or
|
|
* unknown to the implementation, this StringDecoder becomes invalid.
|
|
*
|
|
* \param data The data whose codec should be detected.
|
|
* \param fallbackCodec If the codec of \p data cannot be detected,
|
|
* this \p fallbackCodec is used instead.
|
|
*
|
|
* \sa isValid()
|
|
*/
|
|
void setCodecFromData(const QByteArray& data, const QString& fallbackCodec)
|
|
{
|
|
ensureImpl();
|
|
m_impl->setCodecFromData(data, fallbackCodec);
|
|
}
|
|
|
|
/*! Decodes data with the configured codec.
|
|
*
|
|
* \warning The StringDecoder must be valid when calling decode() or undefined
|
|
* behavior might be invoked.
|
|
*
|
|
* \param data The data to be decoded.
|
|
* \return
|
|
*
|
|
* \sa QTextCodec::toUnicode()
|
|
* \sa QStringDecoder::decode()
|
|
*/
|
|
QString decode(const QByteArray& data) const
|
|
{
|
|
Q_ASSERT_X(isValid(), Q_FUNC_INFO, "Trying to use invalid StringDecoder");
|
|
return m_impl->decode(data);
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
class Impl
|
|
{
|
|
public:
|
|
virtual ~Impl()
|
|
{
|
|
}
|
|
virtual bool isValid() const = 0;
|
|
virtual void setCodec(const QString& codec) = 0;
|
|
virtual void setCodecFromData(const QByteArray& data, const QString& fallbackCodec) = 0;
|
|
virtual QString decode(const QByteArray& data) const = 0;
|
|
virtual Impl* clone() const = 0;
|
|
};
|
|
|
|
#if defined(MOCKNETWORKACCESSMANAGER_QT_HAS_TEXTCODEC)
|
|
class TextCodecImpl : public Impl
|
|
{
|
|
public:
|
|
explicit TextCodecImpl(const QTextCodec* codec = Q_NULLPTR)
|
|
: m_codec(codec)
|
|
{
|
|
}
|
|
virtual bool isValid() const Q_DECL_OVERRIDE
|
|
{
|
|
return m_codec != Q_NULLPTR;
|
|
}
|
|
virtual void setCodec(const QString& codec) Q_DECL_OVERRIDE
|
|
{
|
|
m_codec = QTextCodec::codecForName(codec.toUtf8());
|
|
}
|
|
virtual void setCodecFromData(const QByteArray& data, const QString& fallbackCodec) Q_DECL_OVERRIDE
|
|
{
|
|
m_codec = QTextCodec::codecForUtfText(data, Q_NULLPTR);
|
|
if (!m_codec)
|
|
setCodec(fallbackCodec);
|
|
}
|
|
virtual QString decode(const QByteArray& data) const Q_DECL_OVERRIDE
|
|
{
|
|
Q_ASSERT(m_codec);
|
|
return m_codec->toUnicode(data);
|
|
}
|
|
virtual Impl* clone() const Q_DECL_OVERRIDE
|
|
{
|
|
return new TextCodecImpl(m_codec);
|
|
}
|
|
|
|
private:
|
|
const QTextCodec* m_codec;
|
|
};
|
|
#endif
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
class StringDecoderImpl : public Impl
|
|
{
|
|
public:
|
|
StringDecoderImpl()
|
|
{
|
|
}
|
|
explicit StringDecoderImpl(std::unique_ptr<QStringDecoder>&& decoder)
|
|
: m_decoder(std::move(decoder))
|
|
{
|
|
}
|
|
virtual bool isValid() const Q_DECL_OVERRIDE
|
|
{
|
|
return m_decoder && m_decoder->isValid();
|
|
}
|
|
virtual void setCodec(const QString& codec) Q_DECL_OVERRIDE
|
|
{
|
|
auto encoding = QStringConverter::encodingForName(codec.toUtf8().constData());
|
|
if (encoding) {
|
|
constructQStringDecoder(encoding.value());
|
|
return;
|
|
}
|
|
m_decoder.reset();
|
|
}
|
|
virtual void setCodecFromData(const QByteArray& data, const QString& fallbackCodec) Q_DECL_OVERRIDE
|
|
{
|
|
auto encoding = QStringConverter::encodingForData(data);
|
|
if (encoding) {
|
|
constructQStringDecoder(encoding.value());
|
|
return;
|
|
}
|
|
setCodec(fallbackCodec);
|
|
}
|
|
virtual QString decode(const QByteArray& data) const Q_DECL_OVERRIDE
|
|
{
|
|
Q_ASSERT(m_decoder);
|
|
m_decoder->resetState();
|
|
return m_decoder->decode(data);
|
|
}
|
|
virtual Impl* clone() const Q_DECL_OVERRIDE
|
|
{
|
|
if (!isValid())
|
|
return new StringDecoderImpl{};
|
|
|
|
const auto* encodingName = m_decoder->name();
|
|
Q_ASSERT(encodingName);
|
|
const auto encoding = QStringConverter::encodingForName(encodingName);
|
|
Q_ASSERT(encoding);
|
|
auto cloned = std::make_unique<StringDecoderImpl>();
|
|
cloned->constructQStringDecoder(encoding.value());
|
|
return cloned.release();
|
|
}
|
|
|
|
private:
|
|
void constructQStringDecoder(QStringConverter::Encoding encoding)
|
|
{
|
|
m_decoder = std::make_unique<QStringDecoder>(encoding, QStringConverter::Flag::Stateless);
|
|
}
|
|
std::unique_ptr<QStringDecoder> m_decoder;
|
|
};
|
|
#endif
|
|
//! \endcond
|
|
|
|
private:
|
|
void ensureImpl()
|
|
{
|
|
if (!m_impl) {
|
|
#if defined(MOCKNETWORKACCESSMANAGER_QT_HAS_TEXTCODEC)
|
|
m_impl.reset(new TextCodecImpl());
|
|
#else
|
|
m_impl.reset(new StringDecoderImpl());
|
|
#endif
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<Impl> m_impl;
|
|
};
|
|
|
|
class Rule;
|
|
class MockReplyBuilder;
|
|
template <class Base> class Manager;
|
|
|
|
/*! QList of QByteArray. */
|
|
typedef QList<QByteArray> ByteArrayList;
|
|
/*! QSet of [QNetworkRequest::Attribute].
|
|
* [QNetworkRequest::Attribute]: http://doc.qt.io/qt-5/qnetworkrequest.html#Attribute-enum
|
|
*/
|
|
typedef QSet<QNetworkRequest::Attribute> AttributeSet;
|
|
/*! QHash holding [QNetworkRequest::KnowHeaders] and their corresponding values.
|
|
* \sa QNetworkRequest::header()
|
|
* [QNetworkRequest::KnowHeaders]: http://doc.qt.io/qt-5/qnetworkrequest.html#KnownHeaders-enum
|
|
*/
|
|
typedef QHash<QNetworkRequest::KnownHeaders, QVariant> HeaderHash;
|
|
/*! QSet holding [QNetworkRequest::KnowHeaders].
|
|
* [QNetworkRequest::KnowHeaders]: http://doc.qt.io/qt-5/qnetworkrequest.html#KnownHeaders-enum
|
|
*/
|
|
typedef QSet<QNetworkRequest::KnownHeaders> KnownHeadersSet;
|
|
/*! QHash holding raw headers and their corresponding values.
|
|
* \sa QNetworkRequest::rawHeader()
|
|
*/
|
|
typedef QHash<QByteArray, QByteArray> RawHeaderHash;
|
|
/*! QHash holding query parameter names and their corresponding values.
|
|
* \sa QUrlQuery
|
|
*/
|
|
typedef QHash<QString, QString> QueryParameterHash;
|
|
/*! QHash holding query parameter names and their corresponding values.
|
|
* \sa QUrlQuery
|
|
* \since 0.4.0
|
|
*/
|
|
typedef QHash<QString, QStringList> MultiValueQueryParameterHash;
|
|
/*! QVector of QRegularExpression QPairs.
|
|
*/
|
|
typedef QVector<QPair<QRegularExpression, QRegularExpression>> RegExPairVector;
|
|
|
|
/*! Determines the MIME type of data.
|
|
* \param url The URL of the \p data.
|
|
* \param data The data itself.
|
|
* \return The MIME type of the \p data located at \p url.
|
|
* \sa QMimeDatabase::mimeTypeForFileNameAndData()
|
|
*/
|
|
inline QMimeType guessMimeType(const QUrl& url, const QByteArray& data)
|
|
{
|
|
const QFileInfo fileInfo(url.path());
|
|
return QMimeDatabase().mimeTypeForFileNameAndData(fileInfo.fileName(), data);
|
|
}
|
|
|
|
/*! Provides access to the request data.
|
|
*
|
|
* This mainly groups all the request data into a single struct for convenience.
|
|
*/
|
|
struct Request
|
|
{
|
|
/*! The HTTP request verb.
|
|
*/
|
|
QNetworkAccessManager::Operation operation;
|
|
/*! The QNetworkRequest object.
|
|
* This provides access to the details of the request like URL, headers and attributes.
|
|
*/
|
|
QNetworkRequest qRequest;
|
|
/*! The body data.
|
|
*/
|
|
QByteArray body;
|
|
/*! The timestamp when the Manager began handling the request.
|
|
* For requests received through the public API of QNetworkAccessManager,
|
|
* this can be considered the time when the Manager received the request.
|
|
*/
|
|
QDateTime timestamp;
|
|
|
|
/*! Creates an invalid Request object.
|
|
* \sa isValid()
|
|
*/
|
|
Request()
|
|
: operation(QNetworkAccessManager::CustomOperation)
|
|
{
|
|
}
|
|
|
|
/*! Creates a Request struct.
|
|
* \param op The Request::operation.
|
|
* \param req The Request:.qRequest.
|
|
* \param data The Request::body.
|
|
* \note The Request::timestamp will be set to the current date and time.
|
|
*/
|
|
Request(QNetworkAccessManager::Operation op, const QNetworkRequest& req, const QByteArray& data = QByteArray())
|
|
: operation(op)
|
|
, qRequest(req)
|
|
, body(data)
|
|
, timestamp(QDateTime::currentDateTime())
|
|
{
|
|
}
|
|
|
|
/*! Creates a Request struct.
|
|
* \param req The Request:.qRequest.
|
|
* \param op The Request::operation.
|
|
* \param data The Request::body.
|
|
* \note The Request::timestamp will be set to the current date and time.
|
|
*/
|
|
Request(const QNetworkRequest& req,
|
|
QNetworkAccessManager::Operation op = QNetworkAccessManager::GetOperation,
|
|
const QByteArray& data = QByteArray())
|
|
: operation(op)
|
|
, qRequest(req)
|
|
, body(data)
|
|
, timestamp(QDateTime::currentDateTime())
|
|
{
|
|
}
|
|
|
|
/*! \return \c true if the Request specifies a valid HTTP verb and the qRequest contains a valid URL.
|
|
* The HTTP is not valid if operation is QNetworkAccessManager::CustomOperation
|
|
* and the [QNetworkRequest::CustomVerbAttribute] of qRequest is empty.
|
|
* [QNetworkRequest::CustomVerbAttribute]: http://doc.qt.io/qt-5/qnetworkrequest.html#Attribute-enum
|
|
*/
|
|
bool isValid() const
|
|
{
|
|
return qRequest.url().isValid()
|
|
&& (operation != QNetworkAccessManager::CustomOperation
|
|
|| !qRequest.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray().trimmed().isEmpty());
|
|
}
|
|
|
|
/*! Checks if two Request structs are equal.
|
|
* \param left One Request struct to be compared.
|
|
* \param right The other Request struct to be compared with \p left.
|
|
* \return \c true if all fields of \p left and \c right are equal (including the Request::timestamp).
|
|
* \c false otherwise.
|
|
*/
|
|
friend bool operator==(const Request& left, const Request& right)
|
|
{
|
|
return left.operation == right.operation && left.qRequest == right.qRequest && left.body == right.body
|
|
&& left.timestamp == right.timestamp;
|
|
}
|
|
|
|
/*! Checks if two Request structs differ.
|
|
* \param left One Request struct to be compared.
|
|
* \param right The other Request struct to be compared with \p left.
|
|
* \return \c true if at least one field of \p left and \c right differs (including the Request::timestamp).
|
|
* \c false if \p left and \p right are equal.
|
|
*/
|
|
friend bool operator!=(const Request& left, const Request& right)
|
|
{
|
|
return !(left == right);
|
|
}
|
|
|
|
/*! Returns the operation (HTTP verb) of the request as a string.
|
|
* \return The Request::operation of the Request as a QString or a null `QString()` if the operation is unknown
|
|
* or it is `QNetworkAccessManager::CustomOperation` but the `QNetworkRequest::CustomVerbAttribute` was not set
|
|
* on the Request::qRequest. For the standard operations, the verb is returned in all uppercase letters. For a
|
|
* `CustomOperation`, the verb is return as set in the `QNetworkRequest::CustomVerbAttribute`.
|
|
*/
|
|
QString verb() const
|
|
{
|
|
switch (this->operation) {
|
|
case QNetworkAccessManager::GetOperation:
|
|
return QStringLiteral("GET");
|
|
case QNetworkAccessManager::HeadOperation:
|
|
return QStringLiteral("HEAD");
|
|
case QNetworkAccessManager::PostOperation:
|
|
return QStringLiteral("POST");
|
|
case QNetworkAccessManager::PutOperation:
|
|
return QStringLiteral("PUT");
|
|
case QNetworkAccessManager::DeleteOperation:
|
|
return QStringLiteral("DELETE");
|
|
case QNetworkAccessManager::CustomOperation:
|
|
return this->qRequest.attribute(QNetworkRequest::CustomVerbAttribute).toString();
|
|
// LCOV_EXCL_START
|
|
default:
|
|
qCWarning(log) << "Unknown operation:" << this->operation;
|
|
return QString();
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
}
|
|
};
|
|
|
|
/*! QList of Request structs.*/
|
|
typedef QList<Request> RequestList;
|
|
|
|
/*! Holds the information necessary to make a signal connection.
|
|
*
|
|
* \sa QObject::connect()
|
|
*/
|
|
class SignalConnectionInfo
|
|
{
|
|
public:
|
|
/*! Creates an invalid SignalConnectionInfo object.
|
|
*/
|
|
SignalConnectionInfo()
|
|
: m_sender(Q_NULLPTR)
|
|
, m_connectionType(Qt::AutoConnection)
|
|
{
|
|
}
|
|
|
|
/*! Creates a SignalConnectionInfo for a given object and signal.
|
|
*
|
|
* \param sender The QObject which is the sender of the signal.
|
|
* \param metaSignal The QMetaMethod of the signal.
|
|
* \param connectionType The type of the connection.
|
|
*/
|
|
SignalConnectionInfo(QObject* sender,
|
|
const QMetaMethod& metaSignal,
|
|
Qt::ConnectionType connectionType = Qt::AutoConnection)
|
|
: m_sender(sender)
|
|
, m_signal(metaSignal)
|
|
, m_connectionType(connectionType)
|
|
{
|
|
}
|
|
|
|
/*! \return The sender QObject.
|
|
*/
|
|
QObject* sender() const
|
|
{
|
|
return m_sender;
|
|
}
|
|
|
|
/*! \return The QMetaMethod of the signal.
|
|
*/
|
|
// @sonarcloud-exclude-start
|
|
QMetaMethod signal() const
|
|
// @sonarcloud-exclude-end
|
|
{
|
|
return m_signal;
|
|
}
|
|
|
|
/*! \return The type of the connection.
|
|
*/
|
|
Qt::ConnectionType connectionType() const
|
|
{
|
|
return m_connectionType;
|
|
}
|
|
|
|
/*! \return \c true if this SignalConnectionInfo object contains information allowing to make a valid signal
|
|
* connection. This means that there must be a sender object set and a signal which belongs to this sender
|
|
* object.
|
|
*/
|
|
bool isValid() const
|
|
{
|
|
return m_sender && m_signal.isValid() && m_signal.methodType() == QMetaMethod::Signal
|
|
&& m_sender->metaObject()->method(m_signal.methodIndex()) == m_signal;
|
|
}
|
|
|
|
/*! Creates a connection to the signal described by this %SignalConnectionInfo.
|
|
*
|
|
* \note If this %SignalConnectionInfo object is not valid, the connection will not be established and an
|
|
* invalid QMetaObject::Connection object is returned.
|
|
*
|
|
* \param receiver The receiver QObject.
|
|
* \param slotOrSignal The QMetaMethod of the signal or slot which is connected to the signal described by the
|
|
* this %SignalConnectionInfo. \return The QMetaObject::Connection object as returned by QObject::connect().
|
|
*
|
|
* \sa isValid()
|
|
* \sa QObject::connect()
|
|
*/
|
|
QMetaObject::Connection connect(QObject* receiver, const QMetaMethod& slotOrSignal) const
|
|
{
|
|
return QObject::connect(m_sender, m_signal, receiver, slotOrSignal, m_connectionType);
|
|
}
|
|
|
|
/*! Compares two SignalConnectionInfo objects for equality.
|
|
*
|
|
* \param left One SignalConnectionInfo object.
|
|
* \param right Another SignalConnectionInfo object.
|
|
* \return \c true if \p left and \p right contain the same data.
|
|
*/
|
|
friend bool operator==(const SignalConnectionInfo& left, const SignalConnectionInfo& right)
|
|
{
|
|
return left.m_sender == right.m_sender && left.m_signal == right.m_signal
|
|
&& left.m_connectionType == right.m_connectionType;
|
|
}
|
|
|
|
/*! Compares two SignalConnectionInfo objects for inequality.
|
|
*
|
|
* \param left One SignalConnectionInfo object.
|
|
* \param right Another SignalConnectionInfo object.
|
|
* \return \c true if \p left and \p right contain different data.
|
|
*/
|
|
friend bool operator!=(const SignalConnectionInfo& left, const SignalConnectionInfo& right)
|
|
{
|
|
return !(left == right);
|
|
}
|
|
|
|
private:
|
|
QObject* m_sender;
|
|
QMetaMethod m_signal;
|
|
Qt::ConnectionType m_connectionType;
|
|
};
|
|
|
|
/*! \internal Implementation details
|
|
*/
|
|
namespace detail
|
|
{
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
|
|
|
|
inline bool usesSafeRedirectCustomRequestMethod(const Request& request);
|
|
|
|
/* RFC-7231 defines the request methods GET, HEAD, OPTIONS, and TRACE to be safe
|
|
* for automatic redirection using the same method.
|
|
* See https://tools.ietf.org/html/rfc7231#section-6.4
|
|
* and https://tools.ietf.org/html/rfc7231#section-4.2.1
|
|
*/
|
|
inline bool usesSafeRedirectRequestMethod(const Request& request)
|
|
{
|
|
switch (request.operation) {
|
|
case QNetworkAccessManager::GetOperation:
|
|
case QNetworkAccessManager::HeadOperation:
|
|
return true;
|
|
case QNetworkAccessManager::CustomOperation:
|
|
return usesSafeRedirectCustomRequestMethod(request);
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
inline bool usesSafeRedirectCustomRequestMethod(const Request& request)
|
|
{
|
|
const QString customVerb =
|
|
request.qRequest.attribute(QNetworkRequest::CustomVerbAttribute).toString().toLower();
|
|
return (customVerb == QLatin1String("options") || customVerb == QLatin1String("trace"));
|
|
}
|
|
|
|
#endif // Qt >= 5.6.0
|
|
|
|
inline bool isDataUrlRequest(const Request& request)
|
|
{
|
|
return request.qRequest.url().scheme() == DataUrlUtils::dataScheme();
|
|
}
|
|
|
|
} // namespace detail
|
|
|
|
/*! Mocked QNetworkReply.
|
|
*
|
|
* The MockReply is returned by the Manager instead of a real QNetworkReply.
|
|
* Instead of sending the request to the server and returning the reply,
|
|
* the MockReply returns the predefined (mocked) data.
|
|
*
|
|
* A MockReply behaves like an HTTP based QNetworkReply, except that it doesn't emit
|
|
* the implementation specific signals like QNetworkReplyHttpImpl::startHttpRequest() or
|
|
* QNetworkReplyHttpImpl::abortHttpRequest().
|
|
*/
|
|
class MockReply : public QNetworkReply
|
|
{
|
|
Q_OBJECT
|
|
|
|
template <class Base> friend class Manager;
|
|
friend class MockReplyBuilder;
|
|
friend class Rule;
|
|
|
|
public:
|
|
/*! \return The message body of this reply. */
|
|
QByteArray body() const
|
|
{
|
|
return m_body.data();
|
|
}
|
|
|
|
/*! \return The set of attributes defined on this reply.
|
|
*/
|
|
QSet<QNetworkRequest::Attribute> attributes() const
|
|
{
|
|
return m_attributeSet;
|
|
}
|
|
|
|
/*! \return The signal connection that is used to delay and trigger the finished() signal.
|
|
* If the return signal connection is invalid, the finished() signal is note delayed.
|
|
*/
|
|
SignalConnectionInfo finishDelaySignal() const
|
|
{
|
|
return m_finishDelaySignal;
|
|
}
|
|
|
|
/*! \return \c true
|
|
* \sa QIODevice::isSequential()
|
|
*/
|
|
virtual bool isSequential() const Q_DECL_OVERRIDE
|
|
{
|
|
return true;
|
|
}
|
|
/*! \return The number of bytes available for reading.
|
|
*\sa QIODevice::bytesAvailable()
|
|
*/
|
|
virtual qint64 bytesAvailable() const Q_DECL_OVERRIDE
|
|
{
|
|
return m_body.bytesAvailable();
|
|
}
|
|
|
|
/*! Aborts the simulated network communication.
|
|
* \note At the moment, this method does nothing else than calling close()
|
|
* since the MockReply is already finished before it is returned by the Manager.
|
|
* However, future versions might simulate network communication and then,
|
|
* this method allows aborting that.\n
|
|
* See issue \issue{4}.
|
|
*/
|
|
virtual void abort() Q_DECL_OVERRIDE
|
|
{
|
|
if (this->isRunning()) {
|
|
this->setError(QNetworkReply::OperationCanceledError);
|
|
setFinished(true);
|
|
// TODO: Need to actually finish including emitting signals
|
|
}
|
|
close();
|
|
}
|
|
|
|
/*! Prevents reading further body data from the reply.
|
|
* \sa QNetworkReply::close()
|
|
*/
|
|
virtual void close() Q_DECL_OVERRIDE
|
|
{
|
|
m_body.close();
|
|
QNetworkReply::close();
|
|
}
|
|
|
|
/*! Creates a clone of this reply.
|
|
*
|
|
* \return A new MockReply which has the same properties as this MockReply.
|
|
* The caller takes ownership of the returned object.
|
|
*/
|
|
virtual MockReply* clone() const
|
|
{
|
|
MockReply* clone = new MockReply();
|
|
clone->setBody(this->body());
|
|
clone->setRequest(this->request());
|
|
clone->setUrl(this->url());
|
|
clone->setOperation(this->operation());
|
|
if (m_useDefaultErrorString)
|
|
clone->setError(this->error());
|
|
else
|
|
clone->setError(this->error(), this->errorString());
|
|
clone->setSslConfiguration(this->sslConfiguration());
|
|
clone->setReadBufferSize(this->readBufferSize());
|
|
clone->setBehaviorFlags(this->m_behaviorFlags);
|
|
|
|
clone->copyHeaders(this);
|
|
clone->copyAttributes(this);
|
|
|
|
if (this->isOpen())
|
|
clone->open(this->openMode());
|
|
|
|
clone->setFinished(this->isFinished());
|
|
|
|
clone->m_finishDelaySignal = this->m_finishDelaySignal;
|
|
|
|
return clone;
|
|
}
|
|
|
|
private:
|
|
void copyHeaders(const MockReply* other)
|
|
{
|
|
const QByteArray setCookieHeader = QByteArrayLiteral("Set-Cookie");
|
|
KnownHeadersSet copyKnownHeaders;
|
|
|
|
const ByteArrayList rawHeaders = other->rawHeaderList();
|
|
ByteArrayList::const_iterator rawIter = rawHeaders.cbegin();
|
|
const ByteArrayList::const_iterator rawEnd = rawHeaders.cend();
|
|
for (; rawIter != rawEnd; ++rawIter) {
|
|
if (*rawIter == setCookieHeader) {
|
|
/* Qt doesn't properly concatenate Set-Cookie entries when returning
|
|
* rawHeader(). Therefore, we need to copy that header using header()
|
|
* (see below).
|
|
*/
|
|
copyKnownHeaders.insert(QNetworkRequest::SetCookieHeader);
|
|
continue;
|
|
}
|
|
if (*rawIter == HttpUtils::locationHeader()) {
|
|
const QUrl locationHeader = other->locationHeader();
|
|
if (locationHeader.isValid() && locationHeader.scheme().isEmpty()
|
|
&& locationHeader == other->header(QNetworkRequest::LocationHeader)) {
|
|
/* Due to QTBUG-41061, relative location headers are not set correctly when using
|
|
* setRawHeader(). Therefore, we need to copy that header using header()
|
|
* (see below).
|
|
*/
|
|
copyKnownHeaders.insert(QNetworkRequest::LocationHeader);
|
|
continue;
|
|
}
|
|
}
|
|
this->setRawHeader(*rawIter, other->rawHeader(*rawIter));
|
|
}
|
|
|
|
KnownHeadersSet::const_iterator knownIter = copyKnownHeaders.cbegin();
|
|
const KnownHeadersSet::const_iterator knownEnd = copyKnownHeaders.cend();
|
|
for (; knownIter != knownEnd; ++knownIter) {
|
|
this->setHeader(*knownIter, other->header(*knownIter));
|
|
}
|
|
}
|
|
|
|
void copyAttributes(const MockReply* other)
|
|
{
|
|
AttributeSet::const_iterator iter = other->m_attributeSet.cbegin();
|
|
const AttributeSet::const_iterator end = other->m_attributeSet.cend();
|
|
for (; iter != end; ++iter) {
|
|
this->setAttribute(*iter, other->attribute(*iter));
|
|
}
|
|
}
|
|
|
|
public:
|
|
/*! Checks if this reply indicates a redirect that can be followed automatically.
|
|
* \return \c true if this reply's HTTP status code and \c Location header
|
|
* are valid and the status code indicates a redirect that can be followed automatically.
|
|
*/
|
|
bool isRedirectToBeFollowed() const
|
|
{
|
|
const QVariant statusCodeAttr = this->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
|
if (!statusCodeAttr.isValid())
|
|
return false;
|
|
|
|
const QUrl locationHeaderUrl = this->locationHeader();
|
|
if (!locationHeaderUrl.isValid())
|
|
return false;
|
|
|
|
switch (statusCodeAttr.toInt()) {
|
|
case HttpStatus::MovedPermanently: // 301
|
|
case HttpStatus::Found: // 302
|
|
case HttpStatus::SeeOther: // 303
|
|
case HttpStatus::UseProxy: // 305
|
|
case HttpStatus::TemporaryRedirect: // 307
|
|
return true;
|
|
case HttpStatus::PermanentRedirect: // 308
|
|
if (m_behaviorFlags.testFlag(Behavior_NoAutomatic308Redirect))
|
|
return false; // Qt doesn't recognize 308 for automatic redirection
|
|
else
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/*! Checks if this reply indicates that the request requires authentication.
|
|
* \return \c true if the HTTP status code indicates that the request must be resend
|
|
* with appropriate authentication to succeed.
|
|
*/
|
|
bool requiresAuthentication() const
|
|
{
|
|
switch (this->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()) {
|
|
case HttpStatus::Unauthorized: // 401
|
|
case HttpStatus::ProxyAuthenticationRequired: // 407
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/*! Returns the URL of the HTTP Location header field of a given QNetworkReply.
|
|
* This is a workaround for QTBUG-4106 which prevents that the QNetworkReply::header() method returns a valid
|
|
* QUrl for relative redirection URLs.
|
|
* \param reply The QNetworkReply for which the Location header should be returned.
|
|
* \return The value of the Location header field as a QUrl.
|
|
* \sa https://bugreports.qt.io/browse/QTBUG-41061
|
|
* \since 0.4.0
|
|
*/
|
|
static QUrl locationHeader(const QNetworkReply* reply)
|
|
{
|
|
const QByteArray rawHeader = reply->rawHeader(HttpUtils::locationHeader());
|
|
if (rawHeader.isEmpty())
|
|
return QUrl();
|
|
else
|
|
return QUrl::fromEncoded(rawHeader, QUrl::StrictMode);
|
|
}
|
|
|
|
/*! Returns the URL of the HTTP Location header field.
|
|
*
|
|
* \return The value of the Location header field as a QUrl.
|
|
* \sa locationHeader(const QNetworkReply*)
|
|
* \since 0.4.0
|
|
*/
|
|
QUrl locationHeader() const
|
|
{
|
|
return locationHeader(this);
|
|
}
|
|
|
|
protected:
|
|
/*! Creates a MockReply object.
|
|
* \param parent Parent QObject.
|
|
*/
|
|
explicit MockReply(QObject* parent = Q_NULLPTR)
|
|
: QNetworkReply(parent)
|
|
, m_behaviorFlags(Behavior_Expected)
|
|
, m_redirectCount(0)
|
|
, m_userDefinedError(false)
|
|
, m_useDefaultErrorString(true)
|
|
{
|
|
}
|
|
|
|
/*! Reads bytes from the reply's body.
|
|
* \param[out] data A pointer to an array where the bytes will be written to.
|
|
* \param maxlen The maximum number of bytes that should be read.
|
|
* \return The number of bytes read or -1 if an error occurred.
|
|
* \sa QIODevice::readData()
|
|
*/
|
|
virtual qint64 readData(char* data, qint64 maxlen) Q_DECL_OVERRIDE
|
|
{
|
|
return m_body.read(data, maxlen);
|
|
}
|
|
|
|
/*! Sets the message body of this reply.
|
|
* \param data The body data.
|
|
*/
|
|
void setBody(const QByteArray& data)
|
|
{
|
|
m_body.setData(data);
|
|
}
|
|
|
|
/*! Sets an attribute for this reply.
|
|
* \param attribute The attribute key.
|
|
* \param value The value for the attribute.
|
|
* \sa QNetworkReply::setAttribute()
|
|
*/
|
|
void setAttribute(QNetworkRequest::Attribute attribute, const QVariant& value)
|
|
{
|
|
m_attributeSet.insert(attribute);
|
|
QNetworkReply::setAttribute(attribute, value);
|
|
}
|
|
|
|
/*! Sets the error for this reply.
|
|
*
|
|
* \param error The error code.
|
|
* \param errorString A human-readable string describing the error.
|
|
*/
|
|
void setError(QNetworkReply::NetworkError error, const QString& errorString)
|
|
{
|
|
m_userDefinedError = true;
|
|
if (!errorString.isNull())
|
|
m_useDefaultErrorString = false;
|
|
QNetworkReply::setError(error, errorString);
|
|
}
|
|
|
|
/*! \overload
|
|
* This overload uses %Qt's default error strings for the given \p error code.
|
|
* \param error The error code to be set for this reply.
|
|
*/
|
|
void setError(QNetworkReply::NetworkError error)
|
|
{
|
|
m_userDefinedError = true;
|
|
QNetworkReply::setError(error, this->errorString());
|
|
}
|
|
|
|
protected Q_SLOTS:
|
|
|
|
/*! Prepares the MockReply for returning to the caller of the Manager.
|
|
*
|
|
* This method ensures that this reply has proper values set for the required headers, attributes and
|
|
* properties. For example, it will set the [QNetworkRequest::ContentLengthHeader] and try to guess the correct
|
|
* value for the QNetworkRequest::ContentTypeHeader. However, it will *not* override headers and attributes
|
|
* which have been set explicitly.
|
|
*
|
|
* \param request The request this reply is answering.
|
|
*
|
|
* [QNetworkRequest::ContentLengthHeader]: http://doc.qt.io/qt-5/qnetworkrequest.html#KnownHeaders-enum
|
|
*/
|
|
void prepare(const Request& request)
|
|
{
|
|
if (FileUtils::isFileLikeScheme(request.qRequest.url())) {
|
|
prepareFileLikeReply(request);
|
|
return;
|
|
}
|
|
|
|
prepareHttpLikeReply(request);
|
|
}
|
|
|
|
private:
|
|
void prepareFileLikeReply(const Request& request)
|
|
{
|
|
this->prepareUrlForFileLikeReply(request);
|
|
this->copyPropertiesFromRequest(request);
|
|
this->setAttribute(QNetworkRequest::ConnectionEncryptedAttribute, false);
|
|
|
|
switch (request.operation) {
|
|
case QNetworkAccessManager::GetOperation:
|
|
case QNetworkAccessManager::HeadOperation:
|
|
this->updateContentLengthHeader();
|
|
break;
|
|
case QNetworkAccessManager::PutOperation:
|
|
if (request.qRequest.url().scheme() == FileUtils::qrcScheme())
|
|
this->setAccessDeniedErrorForQrcPutReply(request);
|
|
break;
|
|
default:
|
|
this->setProtocolUnknownError(request);
|
|
break;
|
|
}
|
|
|
|
this->updateErrorString(request);
|
|
}
|
|
|
|
void prepareUrlForFileLikeReply(const Request& request)
|
|
{
|
|
QUrl url = request.qRequest.url();
|
|
if (url.host() == QLatin1String("localhost"))
|
|
url.setHost(QString());
|
|
this->setUrl(url);
|
|
}
|
|
|
|
void prepareHttpLikeReply(const Request& request)
|
|
{
|
|
this->copyPropertiesFromRequest(request);
|
|
|
|
this->setEncryptedAttribute();
|
|
|
|
this->updateContentTypeHeader();
|
|
this->updateContentLengthHeader();
|
|
this->updateHttpStatusCode();
|
|
this->updateHttpReasonPhrase();
|
|
this->updateRedirectionTargetAttribute();
|
|
this->updateErrorString(request);
|
|
}
|
|
|
|
void copyPropertiesFromRequest(const Request& request)
|
|
{
|
|
this->setRequest(request.qRequest);
|
|
if (!this->url().isValid())
|
|
this->setUrl(request.qRequest.url());
|
|
this->setOperation(request.operation);
|
|
this->setSslConfiguration(request.qRequest.sslConfiguration());
|
|
}
|
|
|
|
void setAccessDeniedErrorForQrcPutReply(const Request& request)
|
|
{
|
|
if (m_userDefinedError) {
|
|
if (this->error() == QNetworkReply::ContentAccessDenied && !m_useDefaultErrorString)
|
|
return;
|
|
|
|
qCWarning(log)
|
|
<< "Reply was configured to reply with error" << this->error()
|
|
<< "but a qrc request does not support writing operations and therefore has to reply with"
|
|
<< QNetworkReply::ContentAccessDenied << ". Overriding configured behavior.";
|
|
}
|
|
|
|
QNetworkReply::setError(QNetworkReply::ContentAccessDenied,
|
|
defaultErrorString(QNetworkReply::ContentAccessDenied, request));
|
|
}
|
|
|
|
void setProtocolUnknownError(const Request& request)
|
|
{
|
|
if (m_userDefinedError) {
|
|
if (this->error() == QNetworkReply::ProtocolUnknownError && !m_useDefaultErrorString)
|
|
return;
|
|
|
|
if (FileUtils::isFileLikeScheme(request.qRequest.url())) {
|
|
qCWarning(log) << "Reply was configured to reply with error" << this->error() << "but a request"
|
|
<< request.verb().toUtf8().constData()
|
|
<< request.qRequest.url().toString().toUtf8().constData() << "must be replied with"
|
|
<< QNetworkReply::ProtocolUnknownError << ". Overriding configured behavior.";
|
|
}
|
|
}
|
|
|
|
QNetworkReply::setError(QNetworkReply::ProtocolUnknownError,
|
|
defaultErrorString(QNetworkReply::ProtocolUnknownError, request));
|
|
}
|
|
|
|
void setEncryptedAttribute()
|
|
{
|
|
const QString scheme = this->url().scheme().toLower();
|
|
const bool isEncrypted = scheme == HttpUtils::httpsScheme();
|
|
this->setAttribute(QNetworkRequest::ConnectionEncryptedAttribute, QVariant::fromValue(isEncrypted));
|
|
}
|
|
|
|
void updateContentTypeHeader()
|
|
{
|
|
if (!this->header(QNetworkRequest::ContentTypeHeader).isValid() && !this->body().isEmpty()) {
|
|
const QMimeType mimeType = guessMimeType(this->url(), m_body.data());
|
|
this->setHeader(QNetworkRequest::ContentTypeHeader, QVariant::fromValue(mimeType.name()));
|
|
}
|
|
}
|
|
|
|
void updateContentLengthHeader()
|
|
{
|
|
if (this->rawHeader("Transfer-Encoding").isEmpty()
|
|
&& !this->header(QNetworkRequest::ContentLengthHeader).isValid() && !this->body().isNull()) {
|
|
this->setHeader(QNetworkRequest::ContentLengthHeader, QVariant::fromValue(this->body().length()));
|
|
}
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
|
|
if (!this->attribute(QNetworkRequest::OriginalContentLengthAttribute).isValid()) {
|
|
this->setAttribute(QNetworkRequest::OriginalContentLengthAttribute,
|
|
this->header(QNetworkRequest::ContentLengthHeader));
|
|
}
|
|
#endif // Qt >= 5.9.0
|
|
}
|
|
|
|
void updateHttpStatusCode()
|
|
{
|
|
QVariant statusCodeAttr = this->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
|
if (statusCodeAttr.isValid()) {
|
|
bool canConvertToInt = false;
|
|
statusCodeAttr.toInt(&canConvertToInt);
|
|
if (!canConvertToInt) {
|
|
qCWarning(log) << "Invalid type for HttpStatusCodeAttribute:" << statusCodeAttr.typeName();
|
|
statusCodeAttr = QVariant();
|
|
this->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, statusCodeAttr);
|
|
}
|
|
}
|
|
if (!statusCodeAttr.isValid()) {
|
|
const int statusCode = HttpStatus::networkErrorToStatusCode(this->error());
|
|
if (statusCode > 0) {
|
|
statusCodeAttr = QVariant::fromValue(statusCode);
|
|
this->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, statusCodeAttr);
|
|
}
|
|
}
|
|
}
|
|
|
|
void updateHttpReasonPhrase()
|
|
{
|
|
const QVariant statusCodeAttr = this->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
|
if (!this->attribute(QNetworkRequest::HttpReasonPhraseAttribute).isValid() && statusCodeAttr.isValid()) {
|
|
this->setAttribute(QNetworkRequest::HttpReasonPhraseAttribute,
|
|
HttpStatus::reasonPhrase(statusCodeAttr.toInt()).toUtf8());
|
|
}
|
|
}
|
|
|
|
void updateRedirectionTargetAttribute()
|
|
{
|
|
/* Qt doesn't set the RedirectionTargetAttribute for 305 redirects.
|
|
* See QNetworkReplyHttpImplPrivate::checkForRedirect(const int statusCode)
|
|
*/
|
|
const QVariant statusCodeAttr = this->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
|
if (this->isRedirectToBeFollowed() && statusCodeAttr.toInt() != static_cast<int>(HttpStatus::UseProxy)) {
|
|
const QUrl locationHeaderUrl = this->locationHeader();
|
|
this->setAttribute(QNetworkRequest::RedirectionTargetAttribute, locationHeaderUrl);
|
|
}
|
|
}
|
|
|
|
void updateErrorString(const Request& request)
|
|
{
|
|
if (m_useDefaultErrorString) {
|
|
this->setError(this->error(), defaultErrorString(this->error(), request));
|
|
}
|
|
}
|
|
|
|
QString defaultErrorString(QNetworkReply::NetworkError error, const Request& request) const
|
|
{
|
|
const QString scheme = request.qRequest.url().scheme();
|
|
|
|
if (!FileUtils::isFileLikeScheme(scheme) && useDefaultStatusCodeErrorString(error))
|
|
return defaultStatusCodeErrorString(request);
|
|
|
|
const QString protocol = protocolFromScheme(scheme);
|
|
|
|
const QString protocolSpecificError = protocolSpecificErrorString(error, protocol, request);
|
|
if (!protocolSpecificError.isNull())
|
|
return protocolSpecificError;
|
|
|
|
return protocolCommonErrorString(error, protocol, request);
|
|
}
|
|
|
|
static bool useDefaultStatusCodeErrorString(QNetworkReply::NetworkError errorCode)
|
|
{
|
|
switch (errorCode) {
|
|
case QNetworkReply::UnknownContentError: // other 4xx
|
|
case QNetworkReply::ProtocolInvalidOperationError: // 400
|
|
case QNetworkReply::ContentAccessDenied: // 403
|
|
case QNetworkReply::ContentNotFoundError: // 404
|
|
case QNetworkReply::ContentOperationNotPermittedError: // 405
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 3, 0)
|
|
case QNetworkReply::ContentConflictError: // 409
|
|
case QNetworkReply::ContentGoneError: // 410
|
|
case QNetworkReply::UnknownServerError: // other 5xx
|
|
case QNetworkReply::InternalServerError: // 500
|
|
case QNetworkReply::OperationNotImplementedError: // 501
|
|
case QNetworkReply::ServiceUnavailableError: // 503
|
|
#endif // Qt >= 5.3.0
|
|
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
QString defaultStatusCodeErrorString(const Request& request) const
|
|
{
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 6, 0)
|
|
const QString prefix = QStringLiteral("Error downloading ");
|
|
#else
|
|
const QString prefix = QStringLiteral("Error transferring ");
|
|
#endif
|
|
|
|
return prefix + request.qRequest.url().toDisplayString() + QStringLiteral(" - server replied: ")
|
|
+ this->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
|
|
}
|
|
|
|
static QString protocolFromScheme(const QString& scheme)
|
|
{
|
|
if (scheme == HttpUtils::httpsScheme() || scheme == HttpUtils::httpScheme())
|
|
return HttpUtils::httpScheme();
|
|
if (scheme == FtpUtils::ftpsScheme() || scheme == FtpUtils::ftpScheme())
|
|
return FtpUtils::ftpScheme();
|
|
if (scheme == FileUtils::fileScheme() || scheme == FileUtils::qrcScheme())
|
|
return FileUtils::fileScheme();
|
|
|
|
return QString();
|
|
}
|
|
|
|
static QString
|
|
protocolSpecificErrorString(QNetworkReply::NetworkError error, const QString& protocol, const Request& request)
|
|
{
|
|
if (protocol == FtpUtils::ftpScheme())
|
|
return ftpErrorString(error, request);
|
|
if (protocol == FileUtils::fileScheme())
|
|
return fileErrorString(error, request);
|
|
return fallbackProtocolErrorString(error, request, protocol);
|
|
}
|
|
|
|
static QString ftpErrorString(QNetworkReply::NetworkError error, const Request& request)
|
|
{
|
|
const QString hostName = request.qRequest.url().host();
|
|
|
|
switch (error) {
|
|
case QNetworkReply::ConnectionRefusedError:
|
|
return QCoreApplication::translate("QFtp", "Connection refused to host %1").arg(hostName);
|
|
case QNetworkReply::TimeoutError:
|
|
return QCoreApplication::translate("QFtp", "Connection timed out to host %1").arg(hostName);
|
|
case QNetworkReply::AuthenticationRequiredError:
|
|
return QCoreApplication::translate("QNetworkAccessFtpBackend",
|
|
"Logging in to %1 failed: authentication required")
|
|
.arg(hostName);
|
|
default:
|
|
return QString();
|
|
}
|
|
}
|
|
|
|
static QString fileErrorString(QNetworkReply::NetworkError error, const Request& request)
|
|
{
|
|
const QString scheme = request.qRequest.url().scheme();
|
|
switch (error) {
|
|
case QNetworkReply::ContentOperationNotPermittedError:
|
|
return QCoreApplication::translate("QNetworkAccessFileBackend", "Cannot open %1: Path is a directory")
|
|
.arg(request.qRequest.url().toString());
|
|
case QNetworkReply::ProtocolUnknownError:
|
|
return QCoreApplication::translate("QNetworkReply", "Protocol \"%1\" is unknown").arg(scheme);
|
|
case QNetworkReply::ContentAccessDenied:
|
|
case QNetworkReply::ContentNotFoundError:
|
|
case QNetworkReply::ProtocolFailure:
|
|
default:
|
|
return fileOperationErrorString(error, request);
|
|
}
|
|
}
|
|
|
|
static QString fileOperationErrorString(QNetworkReply::NetworkError error, const Request& request)
|
|
{
|
|
const char* const fileTranslationContext = translationContextForProtocol(FileUtils::fileScheme());
|
|
const QString unknownError = QStringLiteral("Unknown error");
|
|
const QUrl requestUrl = request.qRequest.url();
|
|
if (error == QNetworkReply::ContentNotFoundError
|
|
|| (error == QNetworkReply::ContentAccessDenied
|
|
&& request.operation == QNetworkAccessManager::GetOperation)) {
|
|
QString detailErrorString = QStringLiteral("No such file or directory");
|
|
if (error == QNetworkReply::ContentAccessDenied) {
|
|
if (requestUrl.scheme() == FileUtils::qrcScheme())
|
|
detailErrorString = unknownError;
|
|
else
|
|
detailErrorString = QStringLiteral("Access denied");
|
|
}
|
|
return QCoreApplication::translate(fileTranslationContext, "Error opening %1: %2")
|
|
.arg(requestUrl.toString(), detailErrorString);
|
|
}
|
|
|
|
if (error == QNetworkReply::ProtocolFailure) {
|
|
if (request.operation == QNetworkAccessManager::PutOperation) {
|
|
return QCoreApplication::translate(fileTranslationContext, "Write error writing to %1: %2")
|
|
.arg(requestUrl.toString(), unknownError);
|
|
}
|
|
return QCoreApplication::translate(fileTranslationContext, "Read error reading from %1: %2")
|
|
.arg(requestUrl.toString(), unknownError);
|
|
}
|
|
|
|
return QCoreApplication::translate("QIODevice", "Unknown error");
|
|
}
|
|
|
|
static QString
|
|
fallbackProtocolErrorString(QNetworkReply::NetworkError error, const Request&, const QString& protocol)
|
|
{
|
|
const char* protocolTrContext = translationContextForProtocol(protocol);
|
|
|
|
switch (error) {
|
|
case QNetworkReply::ConnectionRefusedError:
|
|
return QCoreApplication::translate(protocolTrContext, "Connection refused");
|
|
case QNetworkReply::TimeoutError:
|
|
return QCoreApplication::translate("QAbstractSocket", "Socket operation timed out");
|
|
case QNetworkReply::AuthenticationRequiredError: // 401
|
|
return QCoreApplication::translate(protocolTrContext, "Host requires authentication");
|
|
default:
|
|
return QString();
|
|
}
|
|
}
|
|
|
|
static const char* translationContextForProtocol(const QString& protocol)
|
|
{
|
|
if (protocol == HttpUtils::httpScheme())
|
|
return "QHttp";
|
|
if (protocol == FtpUtils::ftpScheme())
|
|
return "QFtp";
|
|
if (protocol == FileUtils::fileScheme())
|
|
return "QNetworkAccessFileBackend";
|
|
return "QNetworkReply";
|
|
}
|
|
|
|
static QString
|
|
protocolCommonErrorString(QNetworkReply::NetworkError error, const QString& protocol, const Request& request)
|
|
{
|
|
const QString hostName = request.qRequest.url().host();
|
|
|
|
switch (error) {
|
|
case QNetworkReply::RemoteHostClosedError:
|
|
return protocolTr(protocol, "Connection closed");
|
|
case QNetworkReply::HostNotFoundError:
|
|
return protocolTr(protocol, "Host %1 not found").arg(hostName);
|
|
case QNetworkReply::OperationCanceledError:
|
|
return QCoreApplication::translate("QNetworkReplyImpl", "Operation canceled");
|
|
case QNetworkReply::SslHandshakeFailedError:
|
|
return protocolTr(protocol, "SSL handshake failed");
|
|
case QNetworkReply::TemporaryNetworkFailureError:
|
|
return qNetworkReplyTr("Temporary network failure.");
|
|
case QNetworkReply::NetworkSessionFailedError:
|
|
return qNetworkReplyTr("Network session error.");
|
|
case QNetworkReply::BackgroundRequestNotAllowedError:
|
|
return qNetworkReplyTr("Background request not allowed.");
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
|
|
case QNetworkReply::TooManyRedirectsError:
|
|
return protocolTr(protocol, "Too many redirects");
|
|
case QNetworkReply::InsecureRedirectError:
|
|
return protocolTr(protocol, "Insecure redirect");
|
|
#endif // Qt >= 5.6.0
|
|
|
|
case QNetworkReply::ProxyConnectionRefusedError:
|
|
return qHttpSocketEngineTr("Proxy connection refused");
|
|
case QNetworkReply::ProxyConnectionClosedError:
|
|
return qHttpSocketEngineTr("Proxy connection closed prematurely");
|
|
case QNetworkReply::ProxyNotFoundError:
|
|
return protocolTr(protocol, "No suitable proxy found");
|
|
case QNetworkReply::ProxyTimeoutError:
|
|
return qHttpSocketEngineTr("Proxy server connection timed out");
|
|
case QNetworkReply::ProxyAuthenticationRequiredError:
|
|
return protocolTr(protocol, "Proxy requires authentication");
|
|
|
|
case QNetworkReply::ProtocolUnknownError:
|
|
return protocolTr(protocol, "Unknown protocol specified");
|
|
case QNetworkReply::ProtocolFailure:
|
|
return protocolTr(protocol, "Data corrupted");
|
|
case QNetworkReply::UnknownNetworkError:
|
|
return QStringLiteral("Unknown network error");
|
|
case QNetworkReply::UnknownProxyError:
|
|
return QStringLiteral("Unknown proxy error");
|
|
|
|
default:
|
|
return QCoreApplication::translate("QIODevice", "Unknown error");
|
|
}
|
|
}
|
|
|
|
static QString protocolTr(const QString& protocol, const char* sourceText)
|
|
{
|
|
const char* protocolTrContext = translationContextForProtocol(protocol);
|
|
return QCoreApplication::translate(protocolTrContext, sourceText);
|
|
}
|
|
|
|
static QString qNetworkReplyTr(const char* sourceText)
|
|
{
|
|
return QCoreApplication::translate("QNetworkReply", sourceText);
|
|
}
|
|
|
|
static QString qHttpSocketEngineTr(const char* sourceText)
|
|
{
|
|
return QCoreApplication::translate("QHttpSocketEngine", sourceText);
|
|
}
|
|
|
|
protected Q_SLOTS:
|
|
/*! Finishes the MockReply and emits signals accordingly.
|
|
*
|
|
* This method will set the reply to finished (see setFinished()), open it for reading and emit the
|
|
* QNetworkReply signals according to the properties of this reply:
|
|
* - QNetworkReply::uploadProgress() to indicate that uploading has finished (if applicable)
|
|
* - QNetworkReply::metaDataChanged() to indicate that the headers of the reply are available
|
|
* - QIODevice::readyRead() and QNetworkReply::downloadProgress() to indicate that the downloading has finished
|
|
* (if applicable)
|
|
* - QNetworkReply::error() to indicate an error (if applicable)
|
|
* - QIODevice::readChannelFinished() and QNetworkReply::finished() to indicate that the reply has finished and
|
|
* is ready to be read
|
|
* - QNetworkReply::finished() to indicate that the reply is complete. Note that this signal is delayed if there
|
|
* is a finish delay configured.
|
|
*
|
|
* \param request The request this reply is answering.
|
|
*/
|
|
void finish(const Request& request)
|
|
{
|
|
m_finalRequest = request;
|
|
if (FileUtils::isFileLikeScheme(m_finalRequest.qRequest.url())) {
|
|
finishFileLikeRequest(m_finalRequest);
|
|
} else {
|
|
finishHttpLikeRequest(m_finalRequest);
|
|
}
|
|
|
|
if (this->m_finishDelaySignal.isValid()) {
|
|
const int communicateFinishSlotIndex = staticMetaObject.indexOfSlot("communicateFinish()");
|
|
Q_ASSERT(communicateFinishSlotIndex != -1);
|
|
const QMetaMethod communicateFinishSlot = staticMetaObject.method(communicateFinishSlotIndex);
|
|
|
|
m_finishDelayConnection = m_finishDelaySignal.connect(this, communicateFinishSlot);
|
|
} else {
|
|
communicateFinish();
|
|
}
|
|
}
|
|
|
|
/*! Tunes the behavior of this MockReply.
|
|
*
|
|
* \param behaviorFlags Combination of BehaviorFlas to define some details of this MockReply's behavior.
|
|
* \note Only certain BehaviorFlags have an effect on a MockReply.
|
|
* \sa BehaviorFlag
|
|
*/
|
|
void setBehaviorFlags(BehaviorFlags behaviorFlags)
|
|
{
|
|
m_behaviorFlags = behaviorFlags;
|
|
}
|
|
|
|
private Q_SLOTS:
|
|
void communicateFinish()
|
|
{
|
|
if (m_finishDelayConnection) {
|
|
QObject::disconnect(m_finishDelayConnection);
|
|
}
|
|
|
|
this->setFinished(true);
|
|
if (emitsFinishedSignals()) {
|
|
this->emitFinishedSignals();
|
|
}
|
|
}
|
|
|
|
private:
|
|
void finishFileLikeRequest(const Request& request)
|
|
{
|
|
if (this->error() == QNetworkReply::ProtocolUnknownError) {
|
|
this->finishFileLikeRequestWithProtocolError(request);
|
|
} else if (request.operation == QNetworkAccessManager::PutOperation) {
|
|
this->finishFileLikePutRequest(request);
|
|
} else if (request.operation == QNetworkAccessManager::HeadOperation) {
|
|
this->finishFileLikeHeadRequest(request);
|
|
}
|
|
|
|
this->openIODeviceForRead();
|
|
}
|
|
|
|
void openIODeviceForRead()
|
|
{
|
|
// Preserve error string because it is reset by QNetworkReply::open() (see issues #37).
|
|
const QString errorString = this->errorString();
|
|
|
|
m_body.open(QIODevice::ReadOnly);
|
|
QNetworkReply::open(QIODevice::ReadOnly);
|
|
|
|
this->setError(this->error(), errorString);
|
|
}
|
|
|
|
void finishFileLikeRequestWithProtocolError(const Request&)
|
|
{
|
|
this->emitErrorSignal();
|
|
this->emitDownloadProgressSignal(0, 0);
|
|
if (m_behaviorFlags.testFlag(Behavior_FinalUpload00Signal)) {
|
|
this->emitUploadProgressSignal(0, 0);
|
|
}
|
|
}
|
|
|
|
void finishFileLikePutRequest(const Request& request)
|
|
{
|
|
m_body.setData(QByteArray());
|
|
|
|
const bool hasError = this->error() != QNetworkReply::NoError;
|
|
if (hasError) {
|
|
this->emitErrorSignal();
|
|
}
|
|
if (!hasError && !request.body.isEmpty()) {
|
|
this->emitUploadProgressSignal(request);
|
|
this->emitDownloadProgressSignal(0, 0);
|
|
} else {
|
|
this->emitDownloadProgressSignal(0, 0);
|
|
this->emitUploadProgressSignal(0, 0);
|
|
}
|
|
}
|
|
|
|
void finishFileLikeHeadRequest(const Request&)
|
|
{
|
|
m_body.setData(QByteArray());
|
|
}
|
|
|
|
void finishHttpLikeRequest(const Request& request)
|
|
{
|
|
if (!request.body.isEmpty()) {
|
|
this->emitUploadProgressSignal(request);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(this, "metaDataChanged", Qt::QueuedConnection);
|
|
|
|
this->openIODeviceForRead();
|
|
|
|
const qint64 replyBodySize = m_body.size();
|
|
if (replyBodySize > 0) {
|
|
QMetaObject::invokeMethod(this, "readyRead", Qt::QueuedConnection);
|
|
this->emitDownloadProgressSignal(replyBodySize, replyBodySize);
|
|
}
|
|
|
|
if (this->error() != QNetworkReply::NoError) {
|
|
emitErrorSignal();
|
|
}
|
|
|
|
this->emitDownloadProgressSignal(replyBodySize, replyBodySize);
|
|
if (m_behaviorFlags.testFlag(Behavior_FinalUpload00Signal) && !request.body.isEmpty()) {
|
|
this->emitUploadProgressSignal(0, 0);
|
|
}
|
|
}
|
|
|
|
void emitDownloadProgressSignal(qint64 received, qint64 total)
|
|
{
|
|
QMetaObject::invokeMethod(
|
|
this, "downloadProgress", Qt::QueuedConnection, Q_ARG(qint64, received), Q_ARG(qint64, total));
|
|
}
|
|
|
|
void emitUploadProgressSignal(qint64 sent, qint64 total)
|
|
{
|
|
QMetaObject::invokeMethod(
|
|
this, "uploadProgress", Qt::QueuedConnection, Q_ARG(qint64, sent), Q_ARG(qint64, total));
|
|
}
|
|
|
|
void emitUploadProgressSignal(const Request& request)
|
|
{
|
|
this->emitUploadProgressSignal(request.body.size(), request.body.size());
|
|
}
|
|
|
|
void emitErrorSignal()
|
|
{
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
|
|
QMetaObject::invokeMethod(
|
|
this, "error", Qt::QueuedConnection, Q_ARG(QNetworkReply::NetworkError, this->error()));
|
|
#else
|
|
QMetaObject::invokeMethod(
|
|
this, "errorOccurred", Qt::QueuedConnection, Q_ARG(QNetworkReply::NetworkError, this->error()));
|
|
#endif
|
|
}
|
|
|
|
void emitFinishedSignals()
|
|
{
|
|
QMetaObject::invokeMethod(this, "readChannelFinished", Qt::QueuedConnection);
|
|
QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection);
|
|
}
|
|
|
|
bool emitsFinishedSignals() const
|
|
{
|
|
if (!FileUtils::isFileLikeScheme(m_finalRequest.qRequest.url())) {
|
|
return true;
|
|
}
|
|
|
|
if (m_finalRequest.operation == QNetworkAccessManager::PutOperation
|
|
|| this->error() == QNetworkReply::ProtocolUnknownError) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QBuffer m_body;
|
|
AttributeSet m_attributeSet;
|
|
BehaviorFlags m_behaviorFlags;
|
|
int m_redirectCount;
|
|
QVector<QUrl> m_followedRedirects;
|
|
bool m_userDefinedError;
|
|
bool m_useDefaultErrorString;
|
|
Request m_finalRequest;
|
|
SignalConnectionInfo m_finishDelaySignal;
|
|
QMetaObject::Connection m_finishDelayConnection;
|
|
};
|
|
|
|
/*! Creates MockReply objects with predefined properties.
|
|
*
|
|
* This class is a configurable factory for MockReply objects.
|
|
* The \c with*() methods configure the properties of the created replies.
|
|
* To create a reply according to the configured properties, call createReply().
|
|
*
|
|
* Similar to the Rule class, the MockReplyBuilder implements a chainable interface for the configuration.
|
|
*/
|
|
class MockReplyBuilder
|
|
{
|
|
public:
|
|
/*! Creates an unconfigured MockReplyBuilder.
|
|
*
|
|
* \note Calling createReply() on an unconfigured MockReplyBuilder will return a \c Q_NULLPTR.
|
|
*/
|
|
MockReplyBuilder()
|
|
: m_replyPrototype(Q_NULLPTR)
|
|
, m_userDefinedError(false)
|
|
{
|
|
}
|
|
|
|
/*! Creates a MockReplyBuilder by copying another one.
|
|
* \param other The MockReplyBuilder which is being copied.
|
|
*/
|
|
MockReplyBuilder(const MockReplyBuilder& other)
|
|
{
|
|
if (other.m_replyPrototype)
|
|
m_replyPrototype.reset(other.m_replyPrototype->clone());
|
|
m_userDefinedError = other.m_userDefinedError;
|
|
}
|
|
|
|
/*! Creates a MockReplyBuilder by moving another one.
|
|
* \param other The MockReplyBuilder which is being moved.
|
|
*/
|
|
MockReplyBuilder(MockReplyBuilder&& other) noexcept
|
|
: MockReplyBuilder()
|
|
{
|
|
swap(other);
|
|
}
|
|
|
|
/*! Destroys this MockReplyBuilder.
|
|
*/
|
|
~MockReplyBuilder()
|
|
{
|
|
// unique_ptr takes care of clean up
|
|
// This destructor just exists to fix SonarCloud cpp:S3624
|
|
}
|
|
|
|
/*! Swaps this MockReplyBuilder with another one.
|
|
* \param other The MockReplyBuilder to be exchanged with this one.
|
|
*/
|
|
void swap(MockReplyBuilder& other)
|
|
{
|
|
m_replyPrototype.swap(other.m_replyPrototype);
|
|
std::swap(m_userDefinedError, other.m_userDefinedError);
|
|
}
|
|
|
|
/*! Swaps two MockReplyBuilders.
|
|
* \param left One MockReplyBuilder to be exchanged.
|
|
* \param right The other MockReplyBuilder to be exchanged.
|
|
*/
|
|
friend void swap(MockReplyBuilder& left, MockReplyBuilder& right)
|
|
{
|
|
left.swap(right);
|
|
}
|
|
|
|
/*! Configures this MockReplyBuilder identical to another one.
|
|
* \param other The MockReplyBuilder whose configuration is being copied.
|
|
* \return \c this
|
|
*/
|
|
MockReplyBuilder& operator=(const MockReplyBuilder& other)
|
|
{
|
|
if (this != &other) {
|
|
if (other.m_replyPrototype)
|
|
m_replyPrototype.reset(other.m_replyPrototype->clone());
|
|
else
|
|
m_replyPrototype.reset();
|
|
m_userDefinedError = other.m_userDefinedError;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
/*! Configures this MockReplyBuilder identical to another one by moving the other one.
|
|
* \param other The MockReplyBuilder which is being moved.
|
|
* \return \c this
|
|
*/
|
|
MockReplyBuilder& operator=(MockReplyBuilder&& other) noexcept
|
|
{
|
|
swap(other);
|
|
return *this;
|
|
}
|
|
|
|
/*! Compares two MockReplyBuilders for equality.
|
|
* \param left One MockReplyBuilder to be compared.
|
|
* \param right The other MockReplyBuilder to be compared.
|
|
* \return \c true if \p left and \p right have the same properties configured
|
|
* and thus create equal MockReply objects.
|
|
*/
|
|
friend bool operator==(const MockReplyBuilder& left, const MockReplyBuilder& right)
|
|
{
|
|
if (&left == &right)
|
|
return true;
|
|
|
|
const MockReply* leftReply = left.m_replyPrototype.get();
|
|
const MockReply* rightReply = right.m_replyPrototype.get();
|
|
|
|
if (leftReply == rightReply)
|
|
return true;
|
|
|
|
if (!leftReply || !rightReply)
|
|
return false;
|
|
|
|
if (leftReply->body() != rightReply->body() || leftReply->rawHeaderPairs() != rightReply->rawHeaderPairs()
|
|
|| leftReply->attributes() != rightReply->attributes() || leftReply->error() != rightReply->error()
|
|
|| leftReply->errorString() != rightReply->errorString()
|
|
|| leftReply->finishDelaySignal() != rightReply->finishDelaySignal())
|
|
return false;
|
|
|
|
const QSet<QNetworkRequest::Attribute> attributes = leftReply->attributes().unite(rightReply->attributes());
|
|
QSet<QNetworkRequest::Attribute>::const_iterator iter = attributes.cbegin();
|
|
const QSet<QNetworkRequest::Attribute>::const_iterator attributeEnd = attributes.cend();
|
|
for (; iter != attributeEnd; ++iter) {
|
|
if (leftReply->attribute(*iter) != rightReply->attribute(*iter))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*! Compares two MockReplyBuilders for inequality.
|
|
* \param left One MockReplyBuilder to be compared.
|
|
* \param right The other MockReplyBuilder to be compared.
|
|
* \return \c true if \p left and \p right have different properties configured
|
|
* and thus create different MockReply objects.
|
|
*/
|
|
friend bool operator!=(const MockReplyBuilder& left, const MockReplyBuilder& right)
|
|
{
|
|
return !(left == right);
|
|
}
|
|
|
|
/*! Configures this MockReplyBuilder identical to another one.
|
|
* This method is identical to the copy operator and exists just to provide a consistent, chainable interface.
|
|
* \param other The MockReplyBuilder which is being copied.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
*/
|
|
MockReplyBuilder& with(const MockReplyBuilder& other)
|
|
{
|
|
*this = other;
|
|
return *this;
|
|
}
|
|
|
|
/*! Configures this MockReplyBuilder identical to another one by moving the other one.
|
|
*
|
|
* This method is identical to the move operator and exists just to provide a consistent, chainable interface.
|
|
*
|
|
* \param other The MockReplyBuilder which is being moved.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
*/
|
|
MockReplyBuilder& with(MockReplyBuilder&& other)
|
|
{
|
|
swap(other);
|
|
return *this;
|
|
}
|
|
|
|
/*! Sets the body for the replies.
|
|
* \param data The data used as the message body for the replies.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
*/
|
|
MockReplyBuilder& withBody(const QByteArray& data)
|
|
{
|
|
ensureReplyPrototype()->setBody(data);
|
|
return *this;
|
|
}
|
|
|
|
/*! Sets the body for the replies to a JSON document.
|
|
* \param json The data used as the message body for the replies.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
*/
|
|
MockReplyBuilder& withBody(const QJsonDocument& json)
|
|
{
|
|
MockReply* proto = ensureReplyPrototype();
|
|
proto->setBody(json.toJson(QJsonDocument::Compact));
|
|
proto->setHeader(QNetworkRequest::ContentTypeHeader,
|
|
QVariant::fromValue(QStringLiteral("application/json")));
|
|
return *this;
|
|
}
|
|
|
|
/*! Sets the body for the replies to the content of a file.
|
|
*
|
|
* The file needs to exist at the time this method is called because the file's
|
|
* content is read and stored in this MockReplyBuilder by this method.
|
|
*
|
|
* This method also tries to determine the file's MIME type using
|
|
* QMimeDatabase::mimeTypeForFileNameAndData() and sets
|
|
* the [QNetworkRequest::ContentTypeHeader] accordingly.
|
|
* If this does not determine the MIME type correctly or if you want to set the
|
|
* MIME type explicitly, use withHeader() or withRawHeader() *after* calling this method.
|
|
*
|
|
* \param filePath The path to the file whose content is used as the message body for the replies.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
* \sa [QNetworkRequest::ContentTypeHeader]
|
|
* \sa withHeader()
|
|
* [QNetworkRequest::ContentTypeHeader]: http://doc.qt.io/qt-5/qnetworkrequest.html#KnownHeaders-enum
|
|
*/
|
|
MockReplyBuilder& withFile(const QString& filePath)
|
|
{
|
|
MockReply* proto = ensureReplyPrototype();
|
|
|
|
QFile file(filePath);
|
|
if (file.open(QIODevice::ReadOnly)) {
|
|
const QByteArray data = file.readAll();
|
|
file.close();
|
|
proto->setBody(data);
|
|
const QMimeType mimeType = QMimeDatabase().mimeTypeForFileNameAndData(filePath, data);
|
|
proto->setHeader(QNetworkRequest::ContentTypeHeader, QVariant::fromValue(mimeType.name()));
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
/*! Sets the status code and reason phrase for the replies.
|
|
*
|
|
* \note \parblock
|
|
* If the \p statusCode is an error code, this will also set the corresponding QNetworkReply::NetworkError
|
|
* unless it was already set using withError(). If no error string is set explicitly, a default error string
|
|
* based on the reason phrase will be set by the Manager before returning the reply. \endparblock
|
|
*
|
|
* \param statusCode The HTTP status code.
|
|
* \param reasonPhrase The HTTP reason phrase. If it is a null QString(), the default reason phrase for the
|
|
* \p statusCode will be used, if available and unless a reason phrase was already set.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
*
|
|
* \sa withError()
|
|
*/
|
|
MockReplyBuilder& withStatus(int statusCode = static_cast<int>(HttpStatus::OK),
|
|
const QString& reasonPhrase = QString())
|
|
{
|
|
MockReply* proto = ensureReplyPrototype();
|
|
proto->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, QVariant::fromValue(statusCode));
|
|
|
|
QString phrase = reasonPhrase;
|
|
if (!phrase.isNull() || !proto->attribute(QNetworkRequest::HttpReasonPhraseAttribute).isValid()) {
|
|
if (phrase.isNull())
|
|
phrase = HttpStatus::reasonPhrase(statusCode);
|
|
proto->setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, QVariant::fromValue(phrase.toUtf8()));
|
|
}
|
|
|
|
if (HttpStatus::isError(statusCode) && !m_userDefinedError)
|
|
proto->setError(HttpStatus::statusCodeToNetworkError(statusCode));
|
|
checkErrorAndStatusCodeConsistency();
|
|
|
|
return *this;
|
|
}
|
|
|
|
/*! Sets a header for the replies.
|
|
*
|
|
* Calling this method with the same header again will override the previous value.
|
|
*
|
|
* \param header The header.
|
|
* \param value The value for the header.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
*
|
|
* \sa QNetworkReply::setHeader()
|
|
*/
|
|
MockReplyBuilder& withHeader(QNetworkRequest::KnownHeaders header, const QVariant& value)
|
|
{
|
|
ensureReplyPrototype()->setHeader(header, value);
|
|
return *this;
|
|
}
|
|
|
|
/*! Sets a raw header for the replies.
|
|
*
|
|
* Calling this method with the same header again will override the previous value.
|
|
* To add multiple header values for the same header, concatenate the values
|
|
* separated by comma. A notable exception from this rule is the \c Set-Cookie
|
|
* header which should be separated by newlines (`\\n`).
|
|
*
|
|
* \param header The header.
|
|
* \param value The value for the header.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
*
|
|
* \sa QNetworkReply::setRawHeader()
|
|
*/
|
|
MockReplyBuilder& withRawHeader(const QByteArray& header, const QByteArray& value)
|
|
{
|
|
ensureReplyPrototype()->setRawHeader(header, value);
|
|
return *this;
|
|
}
|
|
|
|
/*! Sets an attribute for the replies.
|
|
*
|
|
* Calling this method with the same attribute again will override the previous value.
|
|
*
|
|
* \param attribute The attribute.
|
|
* \param value The value for the attribute.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
*/
|
|
MockReplyBuilder& withAttribute(QNetworkRequest::Attribute attribute, const QVariant& value)
|
|
{
|
|
ensureReplyPrototype()->setAttribute(attribute, value);
|
|
return *this;
|
|
}
|
|
|
|
/*! Sets the error for the replies.
|
|
*
|
|
* \note \parblock
|
|
* If the \p error corresponds to a known HTTP status code, the reply returned by the Manager will have the
|
|
* corresponding HTTP status code attribute set if no status code was set explicitly (see withStatus()).\n
|
|
* If both the error code and the HTTP status code are set and they do not match, a warning is issued because
|
|
* this is a state which cannot happen with a real QNetworkReply.
|
|
*
|
|
* If no error string is set explicitly using withError( QNetworkReply::NetworkError, const QString& ), a
|
|
* default error string based on the reason phrase will be set by the Manager before returning the reply.
|
|
*
|
|
* Note that both the automatic setting of the HTTP status code and the error string are not reflected by the
|
|
* MockReply returned by createReply(). Both things are handled by the Manager class and therefore are only
|
|
* reflected by the replies returned from a Manager instance.
|
|
* \endparblock
|
|
*
|
|
* \param error The [QNetworkReply::NetworkError] code.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
*
|
|
* \sa withStatus()
|
|
* [QNetworkReply::NetworkError]: https://doc.qt.io/qt-5/qnetworkreply.html#NetworkError-enum
|
|
*/
|
|
MockReplyBuilder& withError(QNetworkReply::NetworkError error)
|
|
{
|
|
m_userDefinedError = true;
|
|
ensureReplyPrototype()->setError(error);
|
|
|
|
checkErrorAndStatusCodeConsistency();
|
|
return *this;
|
|
}
|
|
|
|
/*! Sets the error and error string for the replies.
|
|
*
|
|
* \note In many cases, it is neither necessary nor desirable to set the error string for the reply explicitly.
|
|
* The Manager sets suitable default error strings for error codes when using withError(
|
|
* QNetworkReply::NetworkError ). However, there can be cases where the default error strings do not match those
|
|
* of a real QNetworkAccessManager (for example when a custom network access manager is used). In such cases,
|
|
* this overload allows setting an explicit error string.
|
|
*
|
|
* \param error The [QNetworkReply::NetworkError] code.
|
|
* \param errorString A message used as error string (see QNetworkReply::errorString()).
|
|
* \return A reference to this %MockReplyBuilder.
|
|
* [QNetworkReply::NetworkError]: https://doc.qt.io/qt-5/qnetworkreply.html#NetworkError-enum
|
|
*
|
|
* \sa withError( QNetworkReply::NetworkError )
|
|
*/
|
|
MockReplyBuilder& withError(QNetworkReply::NetworkError error, const QString& errorString)
|
|
{
|
|
m_userDefinedError = true;
|
|
ensureReplyPrototype()->setError(error, errorString);
|
|
checkErrorAndStatusCodeConsistency();
|
|
return *this;
|
|
}
|
|
|
|
/*! Convenience method to configure redirection for the replies.
|
|
*
|
|
* This sets the [QNetworkRequest::LocationHeader] and the HTTP status code.
|
|
* \note Due to QTBUG-41061, the [QNetworkRequest::LocationHeader] returned by QNetworkReply::header() will be
|
|
* an empty (invalid) URL when \p targetUrl is relative. The redirection will still work as expected.
|
|
* QNetworkReply::rawHeader() always returns the correct value for the Location header.
|
|
*
|
|
* \param targetUrl The URL of the redirection target. Can be relative or absolute.
|
|
* If it is relative, it will be made absolute using the URL of the requests that matched the Rule as base.
|
|
* \param statusCode The HTTP status code to be used. Should normally be in the 3xx range.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
* \sa https://bugreports.qt.io/browse/QTBUG-41061
|
|
* [QNetworkRequest::LocationHeader]: http://doc.qt.io/qt-5/qnetworkrequest.html#KnownHeaders-enum
|
|
*/
|
|
MockReplyBuilder& withRedirect(const QUrl& targetUrl, HttpStatus::Code statusCode = HttpStatus::Found)
|
|
{
|
|
ensureReplyPrototype()->setRawHeader(HttpUtils::locationHeader(), targetUrl.toEncoded());
|
|
withStatus(static_cast<int>(statusCode));
|
|
return *this;
|
|
}
|
|
|
|
/*! Adds an HTTP authentication challenge to the replies and sets their HTTP status code to 401 (Unauthorized).
|
|
*
|
|
* \param authChallenge The authentication challenge to be added to the replies. Must be a valid Challenge or
|
|
* this method does not add the authentication challenge.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
*
|
|
* \sa HttpUtils::Authentication::Challenge::isValid()
|
|
* \sa QNetworkReply::setRawHeader()
|
|
*/
|
|
MockReplyBuilder& withAuthenticate(const HttpUtils::Authentication::Challenge::Ptr& authChallenge)
|
|
{
|
|
MockReply* proto = ensureReplyPrototype();
|
|
if (authChallenge && authChallenge->isValid()) {
|
|
proto->setRawHeader(HttpUtils::wwwAuthenticateHeader(), authChallenge->authenticateHeader());
|
|
withStatus(static_cast<int>(HttpStatus::Unauthorized));
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
/*! Adds an HTTP Basic authentication challenge to the replies and sets their HTTP status code to
|
|
* 401 (Unauthorized).
|
|
*
|
|
* \param realm The realm to be used for the authentication challenge.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
*
|
|
* \sa withAuthenticate(const HttpUtils::Authentication::Challenge::Ptr&)
|
|
*/
|
|
MockReplyBuilder& withAuthenticate(const QString& realm)
|
|
{
|
|
HttpUtils::Authentication::Challenge::Ptr authChallenge(new HttpUtils::Authentication::Basic(realm));
|
|
return withAuthenticate(authChallenge);
|
|
}
|
|
|
|
/*! Adds a cookie to the replies.
|
|
*
|
|
* \note \parblock
|
|
* - The cookie will be appended to the current list of cookies.
|
|
* To replace the complete list of cookies, use withHeader() and set the
|
|
* [QNetworkRequest::SetCookieHeader] to a QList<QNetworkCookie>.
|
|
* - This method does *not* check if a cookie with the same name already
|
|
* exists in the [QNetworkRequest::SetCookieHeader].
|
|
* RFC 6265 says that replies SHOULD NOT contain multiple cookies with the
|
|
* same name. However, to allow simulating misbehaving servers, this method
|
|
* still allows this.
|
|
* \endparblock
|
|
*
|
|
* \param cookie The cookie to be added to the replies.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
*
|
|
* \sa [QNetworkRequest::SetCookieHeader]
|
|
* [QNetworkRequest::SetCookieHeader]: http://doc.qt.io/qt-5/qnetworkrequest.html#KnownHeaders-enum
|
|
*/
|
|
MockReplyBuilder& withCookie(const QNetworkCookie& cookie)
|
|
{
|
|
MockReply* proto = ensureReplyPrototype();
|
|
QList<QNetworkCookie> cookies =
|
|
proto->header(QNetworkRequest::SetCookieHeader).value<QList<QNetworkCookie>>();
|
|
cookies.append(cookie);
|
|
proto->setHeader(QNetworkRequest::SetCookieHeader, QVariant::fromValue(cookies));
|
|
return *this;
|
|
}
|
|
|
|
/*! Adds a delay before the QNetworkReply::finished() signal is emitted.
|
|
*
|
|
* The `finished()` signal of the replies is delay until a given signal is emitted.
|
|
*
|
|
* \note It is important that the given signal is emitted **after** the reply was returned
|
|
* from the manager. If the signal is emitted before the reply is returned from the manager, the reply will
|
|
* never emit the `finished()` signal.
|
|
*
|
|
* \param sender The QObject which emits the signal to wait for with the `finished()` signal.
|
|
* \param signalSignature The signature of the signal to wait for. Note that this should be given **without**
|
|
* using the SIGNAL() macro. So for example simply `builder.withInitialDelayUntil( someObject, "someSignal()"
|
|
* )`. \param connectionType The type of the connection. \return A reference to this %MockReplyBuilder.
|
|
*
|
|
*/
|
|
MockReplyBuilder& withFinishDelayUntil(QObject* sender,
|
|
const char* signalSignature,
|
|
Qt::ConnectionType connectionType = Qt::AutoConnection)
|
|
{
|
|
Q_ASSERT(sender);
|
|
const int signalIndex =
|
|
sender->metaObject()->indexOfSignal(QMetaObject::normalizedSignature(signalSignature).constData());
|
|
Q_ASSERT(signalIndex != -1);
|
|
return withFinishDelayUntil(sender, sender->metaObject()->method(signalIndex), connectionType);
|
|
}
|
|
|
|
/*! \overload
|
|
*
|
|
* \param sender The QObject which emits the signal to wait for with the `finished()` signal.
|
|
* \param metaSignal The QMetaMethod of the signal.
|
|
* \param connectionType The type of the connection.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
*/
|
|
MockReplyBuilder& withFinishDelayUntil(QObject* sender,
|
|
const QMetaMethod& metaSignal,
|
|
Qt::ConnectionType connectionType = Qt::AutoConnection)
|
|
{
|
|
SignalConnectionInfo signalConnection(sender, metaSignal, connectionType);
|
|
Q_ASSERT(signalConnection.isValid());
|
|
ensureReplyPrototype()->m_finishDelaySignal = signalConnection;
|
|
return *this;
|
|
}
|
|
|
|
/*! \overload
|
|
*
|
|
* \tparam PointerToMemberFunction The type of the \p signal.
|
|
* \param sender The QObject which emits the signal to wait for with the `finished()` signal.
|
|
* \param signalPointer The signal to wait for as a function pointer.
|
|
* \param connectionType The type of the connection.
|
|
* \return A reference to this %MockReplyBuilder.
|
|
*/
|
|
template <typename PointerToMemberFunction>
|
|
MockReplyBuilder& withFinishDelayUntil(QObject* sender,
|
|
PointerToMemberFunction signalPointer,
|
|
Qt::ConnectionType connectionType = Qt::AutoConnection)
|
|
{
|
|
const QMetaMethod signalMetaMethod = QMetaMethod::fromSignal(signalPointer);
|
|
Q_ASSERT_X(sender->metaObject()->method(signalMetaMethod.methodIndex()) == signalMetaMethod,
|
|
Q_FUNC_INFO,
|
|
QStringLiteral("Signal '%1' does not belong to class '%2' of sender object.")
|
|
.arg(signalMetaMethod.name(), sender->metaObject()->className())
|
|
.toLatin1()
|
|
.constData());
|
|
return withFinishDelayUntil(sender, signalMetaMethod, connectionType);
|
|
}
|
|
|
|
/*! Creates a reply using the configured properties.
|
|
* \return A new MockReply with properties as configured in this factory or a Q_NULLPTR if no properties have
|
|
* been configured. The caller is responsible for deleting the object when it is not needed anymore.
|
|
*/
|
|
MockReply* createReply() const
|
|
{
|
|
if (m_replyPrototype)
|
|
return m_replyPrototype->clone();
|
|
else
|
|
return Q_NULLPTR;
|
|
}
|
|
|
|
protected:
|
|
/*! Creates a MockReply as prototype if necessary and returns it.
|
|
* \return A MockReply which acts as a prototype for the replies created by createReply().
|
|
* Modify the properties of the returned reply to change the configuration of this factory.
|
|
* The ownership of the returned reply stays with the MockReplyBuilder so do not delete it.
|
|
*/
|
|
MockReply* ensureReplyPrototype()
|
|
{
|
|
if (!m_replyPrototype) {
|
|
m_replyPrototype.reset(new MockReply());
|
|
}
|
|
return m_replyPrototype.get();
|
|
}
|
|
|
|
private:
|
|
void checkErrorAndStatusCodeConsistency() const
|
|
{
|
|
Q_ASSERT(m_replyPrototype);
|
|
const QVariant statusCodeVariant = m_replyPrototype->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
|
if (!statusCodeVariant.isNull() && m_userDefinedError) {
|
|
const int statusCode = statusCodeVariant.toInt();
|
|
const QNetworkReply::NetworkError expectedError = HttpStatus::statusCodeToNetworkError(statusCode);
|
|
if (expectedError != m_replyPrototype->error()) {
|
|
qCWarning(log) << "HTTP status code and QNetworkReply::error() do not match!"
|
|
<< "Status code is" << statusCode << "which corresponds to error" << expectedError
|
|
<< "but actual error is" << m_replyPrototype->error();
|
|
}
|
|
}
|
|
}
|
|
std::unique_ptr<MockReply> m_replyPrototype;
|
|
bool m_userDefinedError;
|
|
};
|
|
|
|
/*! Configuration object for the Manager.
|
|
*
|
|
* The Rule combines predicates for matching requests with a MockReplyBuilder which generates MockReplies when the
|
|
* predicates match.
|
|
*
|
|
* ### Usage ###
|
|
* The Rule implements a chainable interface. This means that the methods return a reference to the Rule
|
|
* itself to allow calling its methods one after the other in one statement.
|
|
* Additionally, the Manager provides convenience methods to create Rule objects.
|
|
* So the typical way to work with Rules is:
|
|
\code
|
|
using namespace MockNetworkAccess;
|
|
using namespace MockNetworkAccess::Predicates;
|
|
Manager< QNetworkAccessManager > mockNAM;
|
|
|
|
mockNAM.whenGet( QUrl( "http://example.com" ) )
|
|
.has( HeaderMatching( QNetworkRequest::UserAgentHeader, QRegularExpression( ".*MyWebBrowser.*" ) ) )
|
|
.reply().withBody( QJsonDocument::fromJson( "{\"response\": \"hello\"}" ) );
|
|
\endcode
|
|
*
|
|
* \note Rule objects cannot be copied but they can be cloned. See clone().
|
|
*
|
|
* ### Matching ###
|
|
* To add predicates to a Rule, use the has() and hasNot() methods.
|
|
* For a Rule to match a request, all its predicates must match. So the predicates have "and" semantics.
|
|
* To achieve "or" semantics, configure multiple Rule in the Manager or implement a dynamic predicate (see
|
|
* \ref page_dynamicMockNam_dynamicPredicates).
|
|
* Since the first matching Rule in the Manager will be used to create a reply, this provides "or" semantics.
|
|
* In addition to negating single Predicates (see hasNot() or Predicate::negate()), the matching of the whole Rule
|
|
* object can be negated by calling negate().
|
|
* \note
|
|
* \parblock
|
|
* The order of the Predicates in a Rule has an impact on the performance of the matching.
|
|
* So, fast Predicates should be added before complex Predicates (for example, Predicates::Header before
|
|
* Predicates::BodyMatching).
|
|
* \endparblock
|
|
*
|
|
* ### Creating Replies ###
|
|
* When a Rule matches a request, the Manager will request it to create a reply for the request.
|
|
* The actual creation of the reply will be done by the Rule's MockReplyBuilder which can be accessed through the
|
|
* reply() method.
|
|
*
|
|
* ### Extending Rule ###
|
|
* Both the matching of requests and the generation of replies can be extended and customized.
|
|
* To extend the matching, implement new Predicate classes.
|
|
* To extend or customize the generation of replies, override the createReply() method. You can then use a
|
|
* MockReplyBuilder to create a reply based on the request.
|
|
* These extension possibilities allow implementing dynamic matching and dynamic replies. That is, depending on the
|
|
* concrete values of the request, the matching behaves differently or the reply has different properties.
|
|
* This also allows introducing state and effectively evolves the Rule into a simple fake server.\n
|
|
* See \ref page_dynamicMockNam for further details.
|
|
*/
|
|
class Rule
|
|
{
|
|
template <class Base> friend class Manager;
|
|
|
|
public:
|
|
/*! Smart pointer to a Rule object. */
|
|
typedef QSharedPointer<Rule> Ptr;
|
|
|
|
/*! Abstract base class for request matching.
|
|
* A Predicate defines a condition which a request must match.
|
|
* If all Predicates of a Rule match the request, the Rule is
|
|
* considered to match the request.
|
|
*
|
|
* To create custom Predicates, derive from this class and implement the private match() method.
|
|
*/
|
|
class Predicate
|
|
{
|
|
public:
|
|
/*! Smart pointer to a Predicate. */
|
|
typedef QSharedPointer<Predicate> Ptr;
|
|
|
|
/*! Default constructor
|
|
*/
|
|
Predicate()
|
|
: m_negate(false)
|
|
{
|
|
}
|
|
|
|
/*! Default destructor
|
|
*/
|
|
virtual ~Predicate()
|
|
{
|
|
}
|
|
|
|
/*! Matches a request against this Predicate.
|
|
* \param request The request to test against this predicate.
|
|
* \return \c true if the Predicate matches the \p request.
|
|
*/
|
|
bool matches(const Request& request)
|
|
{
|
|
return match(request) != m_negate;
|
|
}
|
|
|
|
/*! Negates the matching of this Predicate.
|
|
* \param negate If \c true, the result of matches() is negated before returned.
|
|
*/
|
|
void negate(bool negate = true)
|
|
{
|
|
m_negate = negate;
|
|
}
|
|
|
|
private:
|
|
/*! Performs the actual matching.
|
|
* This method is called by matches() to do the actual matching.
|
|
* \param request The request to be tested to match this %Predicate.
|
|
* \return Must return \c true if the Predicate matches the \p request. Otherwise, \c false.
|
|
*/
|
|
virtual bool match(const Request& request) = 0;
|
|
|
|
bool m_negate;
|
|
};
|
|
|
|
/*! This enum defines the behaviors of a Rule regarding passing matching requests through to the next network
|
|
* access manager.
|
|
*/
|
|
enum PassThroughBehavior
|
|
{
|
|
DontPassThrough, /*!< The rule consumes matching requests and the Manager returns a MockReply
|
|
* generated by the MockReplyBuilder of the rule (see reply()).
|
|
* The request is **not** passed through.\n
|
|
* This is the default behavior.
|
|
*/
|
|
PassThroughReturnMockReply, /*!< The rule passes matching requests through to the next network access
|
|
* manager but the Manager still returns a MockReply generated by the
|
|
* MockReplyBuilder of the rule (see reply()).
|
|
* The reply returned by the next network access manager is discarded.
|
|
* \note If the rule has no reply() configured, matching requests will not
|
|
* be passed through since the Rule is considered "invalid" by the Manager.
|
|
*/
|
|
PassThroughReturnDelegatedReply /*!< The rule passes matching requests through to the next network access
|
|
* manager and the Manager returns the reply returned by the next network
|
|
* access manager.
|
|
*/
|
|
};
|
|
|
|
/*! Creates a Rule which matches every request but creates no replies.
|
|
*
|
|
* In regard to the Manager, such a Rule is invalid and is ignored by the Manager.
|
|
* To make it valid, configure the MockReplyBuilder returned by reply().
|
|
* \sa Manager
|
|
*/
|
|
Rule()
|
|
: m_negate(false)
|
|
, m_passThroughBehavior(DontPassThrough)
|
|
{
|
|
}
|
|
|
|
/*! Deleted copy constructor.
|
|
*/
|
|
Rule(const Rule&) = delete;
|
|
|
|
/*! Default move operator.
|
|
*/
|
|
Rule(Rule&&) = default;
|
|
|
|
/*! Default destructor.
|
|
*/
|
|
virtual ~Rule() = default;
|
|
|
|
/*! Deleted assignment operator.
|
|
*/
|
|
Rule& operator=(const Rule&) = delete;
|
|
|
|
public:
|
|
/*! Negates the matching of this rule.
|
|
* \param negate If \c true, the result of the matching is negated, meaning if _any_ of the predicates does
|
|
* _not_ match, this Rule matches. If \c false, the negation is removed reverting to normal "and" semantics.
|
|
* \return A reference to this %Rule.
|
|
* \sa matches()
|
|
*/
|
|
Rule& negate(bool negate = true)
|
|
{
|
|
m_negate = negate;
|
|
return *this;
|
|
}
|
|
|
|
/*! \return \c true if this rule negates the matching. \c false otherwise.
|
|
*
|
|
* \sa negate()
|
|
*/
|
|
bool isNegated() const
|
|
{
|
|
return m_negate;
|
|
}
|
|
|
|
/*! Adds a Predicate to the Rule.
|
|
* \tparam PredicateType The type of the \p predicate. \p PredicateType must be move-constructable (if
|
|
* \p predicate is an rvalue reference) or copy-constructable (if \p predicate is an lvalue reference) for
|
|
* this method to work.
|
|
* \param predicate The Predicate to be added to the Rule.
|
|
* Note that \p predicate will be copied/moved and the resulting Predicate is actually added to the Rule.
|
|
* \return A reference to this %Rule.
|
|
*/
|
|
template <class PredicateType> Rule& has(PredicateType&& predicate)
|
|
{
|
|
m_predicates.append(Predicate::Ptr(
|
|
new typename std::remove_const<typename std::remove_reference<PredicateType>::type>::type(
|
|
std::forward<PredicateType>(predicate))));
|
|
return *this;
|
|
}
|
|
|
|
/*! Adds a Predicate to the Rule.
|
|
* \param predicate Smart pointer to the Predicate to be added to the Rule.
|
|
* \return A reference to this %Rule.
|
|
*/
|
|
Rule& has(const Predicate::Ptr& predicate)
|
|
{
|
|
m_predicates.append(predicate);
|
|
return *this;
|
|
}
|
|
|
|
/*! Negates a Predicate and adds it to the Rule.
|
|
* \tparam PredicateType The type of the \p predicate. \p PredicateType must be move-constructable (if
|
|
* \p predicate is an rvalue reference) or copy-constructable (if \p predicate is an lvalue reference) for
|
|
* this method to work.
|
|
* \param predicate The Predicate to be negated and added to the Rule.
|
|
* Note that \p predicate will be copied and the copy is negated and added.
|
|
* \return A reference to this %Rule.
|
|
*/
|
|
template <class PredicateType> Rule& hasNot(PredicateType&& predicate)
|
|
{
|
|
Predicate::Ptr copy(new
|
|
typename std::remove_const<typename std::remove_reference<PredicateType>::type>::type(
|
|
std::forward<PredicateType>(predicate)));
|
|
copy->negate();
|
|
m_predicates.append(copy);
|
|
return *this;
|
|
}
|
|
|
|
/*! Negates a Predicate and adds it to the Rule.
|
|
* \param predicate Smart pointer to the Predicate to be negated and added to the Rule.
|
|
* \return A reference to this %Rule.
|
|
* \sa Predicate::negate()
|
|
*/
|
|
Rule& hasNot(const Predicate::Ptr& predicate)
|
|
{
|
|
predicate->negate();
|
|
m_predicates.append(predicate);
|
|
return *this;
|
|
}
|
|
|
|
/*! Creates a \link Predicates::Generic Generic Predicate \endlink and adds it to this Rule.
|
|
*
|
|
* Example:
|
|
* \code
|
|
* Manager< QNetworkAccessManager > mnam;
|
|
* mnam.whenPost( QUrl( "http://example.com/json" ) )
|
|
* .isMatching( [] ( const Request& request ) -> bool {
|
|
* if ( request.body.isEmpty()
|
|
* || request.qRequest.header( QNetworkRequest::ContentTypeHeader ).toString() != "application/json" )
|
|
* return true;
|
|
* QJsonDocument jsonDoc = QJsonDocument::fromJson( request.body );
|
|
* return jsonDoc.isNull();
|
|
* } )
|
|
* .reply().withError( QNetworkReply::ProtocolInvalidOperationError, "Expected a JSON body" );
|
|
* \endcode
|
|
*
|
|
* \tparam Matcher The type of the callable object.
|
|
* \param matcher The callable object used to create the Generic predicate.
|
|
* \return A reference to this %Rule.
|
|
* \sa isNotMatching()
|
|
* \sa Predicates::Generic
|
|
* \sa Predicates::createGeneric()
|
|
*/
|
|
template <class Matcher> Rule& isMatching(const Matcher& matcher);
|
|
|
|
/*! Creates a \link Predicates::Generic Generic Predicate \endlink, negates it and adds it to this Rule.
|
|
*
|
|
* See isMatching() for a usage example.
|
|
*
|
|
* \tparam Matcher The type of the callable object.
|
|
* \param matcher The callable object used to create the Generic predicate.
|
|
* \return A reference to this %Rule.
|
|
* \sa isMatching()
|
|
* \sa Predicates::Generic
|
|
* \sa Predicates::createGeneric()
|
|
*/
|
|
template <class Matcher> Rule& isNotMatching(const Matcher& matcher);
|
|
|
|
/*! \return The predicates of this Rule.
|
|
*/
|
|
QVector<Predicate::Ptr> predicates() const
|
|
{
|
|
return m_predicates;
|
|
}
|
|
|
|
/*! Sets the predicates of this Rule.
|
|
* This removes all previous Predicates of this Rule.
|
|
* \param predicates The new Predicates for this Rule.
|
|
*/
|
|
void setPredicates(const QVector<Predicate::Ptr>& predicates)
|
|
{
|
|
m_predicates = predicates;
|
|
}
|
|
|
|
/*! \return The MockReplyBuilder used to create replies in case this Rule matches. Use the returned builder to
|
|
* configure the replies.
|
|
*/
|
|
MockReplyBuilder& reply()
|
|
{
|
|
return m_replyBuilder;
|
|
}
|
|
|
|
/*! Defines whether matching requests should be passed through to the next network access manager.
|
|
* \param behavior How the Rule should behave in regard to passing requests through.
|
|
* \param passThroughManager The network access manager to which requests are passed through.
|
|
* If this is null, the pass through manager of this Rule's manager is used to pass requests through (see
|
|
* Manager::setPassThroughNam()).
|
|
* \n **Since** 0.4.0
|
|
* \return A reference to this %Rule.
|
|
* \sa PassThroughBehavior
|
|
* \sa \ref page_passThrough
|
|
*/
|
|
Rule& passThrough(PassThroughBehavior behavior = PassThroughReturnDelegatedReply,
|
|
QNetworkAccessManager* passThroughManager = Q_NULLPTR)
|
|
{
|
|
m_passThroughBehavior = behavior;
|
|
m_passThroughManager = passThroughManager;
|
|
return *this;
|
|
}
|
|
|
|
/*! \return Whether this rule passes matching requests through to the next network access manager and what
|
|
* is returned by the Manager if the request is passed through.
|
|
*
|
|
* \sa PassThroughBehavior
|
|
*/
|
|
PassThroughBehavior passThroughBehavior() const
|
|
{
|
|
return m_passThroughBehavior;
|
|
}
|
|
|
|
/*! \return The network access manager to which matching requests are passed through.
|
|
* \sa passThrough()
|
|
* \sa PassThroughBehavior
|
|
* \since 0.4.0
|
|
*/
|
|
QNetworkAccessManager* passThroughManager() const
|
|
{
|
|
return m_passThroughManager;
|
|
}
|
|
|
|
/*! Matches a request against the predicates of this Rule.
|
|
* \param request The request to be tested against the predicates.
|
|
* \return \c true if the \p request matches all predicates.
|
|
*/
|
|
bool matches(const Request& request) const
|
|
{
|
|
bool returnValue = true;
|
|
|
|
QVector<Predicate::Ptr>::const_iterator iter = m_predicates.cbegin();
|
|
const QVector<Predicate::Ptr>::const_iterator end = m_predicates.cend();
|
|
for (; iter != end; ++iter) {
|
|
if (!(*iter)->matches(request)) {
|
|
returnValue = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return returnValue != m_negate;
|
|
}
|
|
|
|
/*! Creates a MockReply using the MockReplyBuilder of this Rule.
|
|
*
|
|
* The base implementation simply calls MockReplyBuilder::createReply().
|
|
*
|
|
* \note When you reimplement this method, you can also return a null pointer. In that case, it is treated as if
|
|
* the Rule didn't match the request. This is useful if you create the replies dynamically and get into a
|
|
* situation where you cannot generate an appropriate reply.
|
|
*
|
|
* \param request The request to be answered.
|
|
* \return A new MockReply object created by the MockReplyBuilder (see reply()).
|
|
* The caller takes ownership of the returned MockReply and should delete it
|
|
* when it is not needed anymore.
|
|
*/
|
|
virtual MockReply* createReply(const Request& request)
|
|
{
|
|
Q_UNUSED(request)
|
|
return m_replyBuilder.createReply();
|
|
}
|
|
|
|
/*! Creates a clone of this Rule.
|
|
*
|
|
* \return A Rule object with the same properties as this Rule except for the matchedRequests().
|
|
* Note that the predicates() are shallow copied meaning that this Rule and the clone will have pointers to
|
|
* the same Predicate objects. All other properties except for the matchedRequests() are copied.
|
|
* The caller is taking ownership of the returned Rule object and should delete it when it is not needed
|
|
* anymore.
|
|
*/
|
|
virtual Rule* clone() const
|
|
{
|
|
Rule* cloned = new Rule();
|
|
cloned->m_predicates = m_predicates;
|
|
cloned->m_replyBuilder = m_replyBuilder;
|
|
cloned->m_negate = m_negate;
|
|
cloned->m_passThroughBehavior = m_passThroughBehavior;
|
|
cloned->m_passThroughManager = m_passThroughManager;
|
|
return cloned;
|
|
}
|
|
|
|
/*! \return The requests that matched this Rule.
|
|
*/
|
|
QVector<Request> matchedRequests() const
|
|
{
|
|
return m_matchedRequests;
|
|
}
|
|
|
|
private:
|
|
QVector<Predicate::Ptr> m_predicates;
|
|
MockReplyBuilder m_replyBuilder;
|
|
bool m_negate;
|
|
PassThroughBehavior m_passThroughBehavior;
|
|
QVector<Request> m_matchedRequests;
|
|
QPointer<QNetworkAccessManager> m_passThroughManager;
|
|
};
|
|
|
|
/*! Namespace for the matching predicates provided by the MockNetworkAccessManager library.
|
|
* \sa Rule::Predicate
|
|
*/
|
|
namespace Predicates
|
|
{
|
|
/*! Matches any request.
|
|
* This is useful to handle unexpected requests.
|
|
*/
|
|
class Anything : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate which matches any request.
|
|
*/
|
|
Anything()
|
|
: Predicate()
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request&) Q_DECL_OVERRIDE
|
|
{
|
|
return true;
|
|
}
|
|
//! \endcond
|
|
};
|
|
|
|
/*! Matches if a given callable object matches the request.
|
|
*
|
|
* Normally, this class does not need to be used directly since there are the
|
|
* convenience methods Rule::isMatching() and Rule::isNotMatching().
|
|
*
|
|
* If this class should still be used directly and the compiler does not support
|
|
* class template argument deduction, there is the convenience method createGeneric().
|
|
*
|
|
* \tparam Matcher A callable type which is used to match the request.
|
|
* The \p Matcher must accept a `const Request&` as parameter and return a `bool`.
|
|
* When the predicate is tested against a request, the \p Matcher is invoked
|
|
* and its return value defines whether this predicate matches.
|
|
*
|
|
* \sa createGeneric()
|
|
* \sa \ref page_dynamicMockNam_dynamicPredicates_examples_2
|
|
*/
|
|
template <class Matcher> class Generic : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching using a callable object.
|
|
* \param matcher The callable object which is invoked to match the request.
|
|
*/
|
|
explicit Generic(const Matcher& matcher)
|
|
: Predicate()
|
|
, m_matcher(matcher)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
return m_matcher(request);
|
|
}
|
|
//! \endcond
|
|
|
|
Matcher m_matcher;
|
|
};
|
|
|
|
/*! Creates a Generic predicate.
|
|
* This factory method mainly exists to take advantage of template argument deduction when creating a Generic
|
|
* predicate.
|
|
* \tparam Matcher The type of the callable object. Must take a single \c const Request& parameter and
|
|
* return a \c bool.
|
|
* \param matcher The callable object. Must return \c true if the predicate matches the Request given as
|
|
* parameter. \return A smart pointer to a Generic predicate created with \p matcher. \sa Generic
|
|
*/
|
|
template <class Matcher> inline Rule::Predicate::Ptr createGeneric(const Matcher& matcher)
|
|
{
|
|
return Rule::Predicate::Ptr(new Generic<Matcher>(matcher));
|
|
}
|
|
|
|
/*! Matches if the HTTP verb equals a given verb.
|
|
*/
|
|
class Verb : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching the HTTP verb.
|
|
* \param operation The verb to match.
|
|
* \param customVerb If \p operation is QNetworkAccessManager::CustomOperation, \p customVerb defines the
|
|
* custom verb to match.
|
|
* In other cases, this parameter is ignored.
|
|
*/
|
|
explicit Verb(QNetworkAccessManager::Operation operation, const QByteArray& customVerb = QByteArray())
|
|
: Predicate()
|
|
, m_operation(operation)
|
|
{
|
|
if (m_operation == QNetworkAccessManager::CustomOperation)
|
|
m_customVerb = customVerb;
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
if (request.operation != m_operation)
|
|
return false;
|
|
if (request.operation == QNetworkAccessManager::CustomOperation
|
|
&& request.qRequest.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray() != m_customVerb)
|
|
return false;
|
|
return true;
|
|
}
|
|
//! \endcond
|
|
|
|
QNetworkAccessManager::Operation m_operation;
|
|
QByteArray m_customVerb;
|
|
};
|
|
|
|
/*! Matches if the request URL matches a regular expression.
|
|
* \note To match query parameters, it is typically easier to use the predicate QueryParameters.
|
|
*/
|
|
class UrlMatching : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching the request URL against a regular expression.
|
|
* \param urlRegEx The regular expression.
|
|
* \param format QUrl::FormattingOptions to be used to convert the QUrl to a QString when matching the
|
|
* regular expression. The default is QUrl::PrettyDecoded since it is also the default for QUrl::toString().
|
|
* Note that QUrl::FullyDecoded does *not* work since QUrl::toString() does not permit it.
|
|
*/
|
|
explicit UrlMatching(const QRegularExpression& urlRegEx,
|
|
QUrl::FormattingOptions format = QUrl::FormattingOptions(QUrl::PrettyDecoded))
|
|
: Predicate()
|
|
, m_urlRegEx(urlRegEx)
|
|
, m_format(format)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
const QString url = request.qRequest.url().toString(m_format);
|
|
return m_urlRegEx.match(url).hasMatch();
|
|
}
|
|
//! \endcond
|
|
|
|
QRegularExpression m_urlRegEx;
|
|
QUrl::FormattingOptions m_format;
|
|
};
|
|
|
|
/*! Matches if the request URL equals a given URL.
|
|
* \note This predicate does an exact matching of the URL so it is stricter than the other URL predicates.
|
|
*/
|
|
class Url : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching if the request URL equals a given URL.
|
|
* \note Invalid QUrls are treated like empty QUrls for the comparison.
|
|
* In other words, the following QUrl objects are all considered equal: `QUrl()`, `QUrl("")`,
|
|
* `QUrl("http://..")`, `QUrl("http://!!")`
|
|
* \param url The URL which is compared against the request URL.
|
|
* \param defaultPort Allows defining a default port to be considered when the request or \p url does not
|
|
* specify a port explicitly.
|
|
* The default ports for HTTP (80), HTTPS (443) and FTP (21) are used when no \p defaultPort was
|
|
* specified (that is, when \p defaultPort is -1) and the \p url has a matching scheme.
|
|
*/
|
|
explicit Url(const QUrl& url, int defaultPort = -1)
|
|
: Predicate()
|
|
, m_url(url)
|
|
, m_defaultPort(defaultPort)
|
|
{
|
|
detectDefaultPort();
|
|
}
|
|
|
|
/*! \overload
|
|
*
|
|
* \param url The URL compared against the request URL. If it is empty, it always matches.
|
|
* \param defaultPort Allows defining a default port to be considered when the request or \p url does not
|
|
* specify a port explicitly.
|
|
* The default ports for HTTP (80) and HTTPS (443) are used when no \p defaultPort was specified.
|
|
*/
|
|
explicit Url(const QString& url, int defaultPort = -1)
|
|
: Predicate()
|
|
, m_url(url)
|
|
, m_defaultPort(defaultPort)
|
|
{
|
|
detectDefaultPort();
|
|
}
|
|
|
|
private:
|
|
void detectDefaultPort()
|
|
{
|
|
if (m_defaultPort == -1) {
|
|
const QString urlProtocol = m_url.scheme().toLower();
|
|
if (urlProtocol == HttpUtils::httpScheme())
|
|
m_defaultPort = HttpUtils::HttpDefaultPort;
|
|
else if (urlProtocol == HttpUtils::httpsScheme())
|
|
m_defaultPort = HttpUtils::HttpsDefaultPort;
|
|
else if (urlProtocol == FtpUtils::ftpScheme())
|
|
m_defaultPort = FtpUtils::FtpDefaultPort;
|
|
}
|
|
}
|
|
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
const QUrl requestUrl = request.qRequest.url();
|
|
return (requestUrl == m_url)
|
|
|| (m_defaultPort > -1
|
|
/* QUrl::matches() could be used here instead of QUrl::adjusted() but it is buggy:
|
|
* https://bugreports.qt.io/browse/QTBUG-70774
|
|
&& m_url.matches(requestUrl, QUrl::RemovePort)
|
|
*/
|
|
&& m_url.adjusted(QUrl::RemovePort) == requestUrl.adjusted(QUrl::RemovePort)
|
|
&& m_url.port(m_defaultPort) == requestUrl.port(m_defaultPort));
|
|
}
|
|
//! \endcond
|
|
|
|
QUrl m_url;
|
|
int m_defaultPort;
|
|
};
|
|
|
|
/*! Matches if the request URL contains a given query parameter.
|
|
* Note that the URL can contain more query parameters. This predicate just checks that the given parameter
|
|
* exists with the given value.
|
|
*
|
|
* This predicate is especially useful in combination with the regular expression predicate UrlMatching()
|
|
* since query parameters typically don't have a defined order which makes it very hard to match them with
|
|
* regular expressions.
|
|
*/
|
|
class QueryParameter : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching a URL query parameter.
|
|
* \param key The name of the query parameter.
|
|
* \param value The value that the query parameter needs to have.
|
|
* \param format QUrl::ComponentFormattingOptions used to convert the query parameter value to a QString.
|
|
* The default is QUrl::PrettyDecoded since it is also the default for QUrlQuery::queryItemValue().
|
|
*/
|
|
explicit QueryParameter(
|
|
const QString& key,
|
|
const QString& value,
|
|
QUrl::ComponentFormattingOptions format = QUrl::ComponentFormattingOptions(QUrl::PrettyDecoded))
|
|
: Predicate()
|
|
, m_key(key)
|
|
, m_values(value)
|
|
, m_format(format)
|
|
{
|
|
}
|
|
|
|
/*! Creates a predicate matching a URL query parameter with a list of values.
|
|
* \param key The name of the query parameter.
|
|
* \param values The values that the query parameter needs to have in the order they appear in the query.
|
|
* \param format QUrl::ComponentFormattingOptions used to convert the query parameter value to a QString.
|
|
* The default is QUrl::PrettyDecoded since it is also the default for QUrlQuery::queryItemValue().
|
|
* \since 0.4.0
|
|
*/
|
|
explicit QueryParameter(
|
|
const QString& key,
|
|
const QStringList& values,
|
|
QUrl::ComponentFormattingOptions format = QUrl::ComponentFormattingOptions(QUrl::PrettyDecoded))
|
|
: Predicate()
|
|
, m_key(key)
|
|
, m_values(values)
|
|
, m_format(format)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
const QUrlQuery query(request.qRequest.url());
|
|
return query.hasQueryItem(m_key) && query.allQueryItemValues(m_key, m_format) == m_values;
|
|
}
|
|
//! \endcond
|
|
|
|
QString m_key;
|
|
QStringList m_values;
|
|
QUrl::ComponentFormattingOptions m_format;
|
|
};
|
|
|
|
/*! Matches if the request URL contains a given query parameter with a value matching a given regular
|
|
* expression. If the query parameter contains multiple values, **all** of its values must match the given
|
|
* regular expression.
|
|
*
|
|
* Note that the URL can contain more query parameters. This predicate just checks that the given parameter
|
|
* exists with a matching value.
|
|
*
|
|
* This predicate is especially useful in combination with the regular expression predicate UrlMatching()
|
|
* since query parameters typically don't have a defined order which makes it very hard to match them with
|
|
* regular expressions.
|
|
*/
|
|
class QueryParameterMatching : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching an URL query parameter value .
|
|
* \param key The name of the query parameter.
|
|
* \param regEx The regular expression matched against the query parameter value.
|
|
* \param format QUrl::ComponentFormattingOptions to be used to convert the query parameter value to a
|
|
* QString when matching the regular expression. The default is QUrl::PrettyDecoded since it is also the
|
|
* default for QUrlQuery::queryItemValue().
|
|
*/
|
|
explicit QueryParameterMatching(
|
|
const QString& key,
|
|
const QRegularExpression& regEx,
|
|
QUrl::ComponentFormattingOptions format = QUrl::ComponentFormattingOptions(QUrl::PrettyDecoded))
|
|
: Predicate()
|
|
, m_key(key)
|
|
, m_regEx(regEx)
|
|
, m_format(format)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
const QUrlQuery query(request.qRequest.url());
|
|
if (!query.hasQueryItem(m_key))
|
|
return false;
|
|
|
|
const QStringList values = query.allQueryItemValues(m_key);
|
|
QStringList::const_iterator iter = values.cbegin();
|
|
const QStringList::const_iterator end = values.cend();
|
|
for (; iter != end; ++iter) {
|
|
if (!m_regEx.match(*iter).hasMatch())
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
//! \endcond
|
|
|
|
QString m_key;
|
|
QRegularExpression m_regEx;
|
|
QUrl::ComponentFormattingOptions m_format;
|
|
};
|
|
|
|
/*! Matches if the request URL contains given query parameters.
|
|
* Note that the URL can contain more query parameters. This predicate just checks that the given parameters
|
|
* exist with the given values.
|
|
*
|
|
* This predicate is especially useful in combination with the regular expression predicate UrlMatching()
|
|
* since query parameters typically don't have a defined order which makes it very hard to match them with
|
|
* regular expressions.
|
|
*/
|
|
class QueryParameters : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching URL query parameters.
|
|
* \param parameters A QHash of query parameters that need to be present in the URL with defined values.
|
|
* The keys of the hash are the expected parameter names and the corresponding values of the hash are the
|
|
* expected parameter values.
|
|
* \param format QUrl::ComponentFormattingOptions used to convert the query parameter value to a QString.
|
|
* The default is QUrl::PrettyDecoded since it is also the default for QUrlQuery::queryItemValue().
|
|
*/
|
|
explicit QueryParameters(
|
|
const QueryParameterHash& parameters,
|
|
QUrl::ComponentFormattingOptions format = QUrl::ComponentFormattingOptions(QUrl::PrettyDecoded))
|
|
: Predicate()
|
|
, m_format(format)
|
|
{
|
|
QueryParameterHash::const_iterator iter = parameters.cbegin();
|
|
const QueryParameterHash::const_iterator end = parameters.cend();
|
|
for (; iter != end; ++iter) {
|
|
m_queryParameters.insert(iter.key(), QStringList() << iter.value());
|
|
}
|
|
}
|
|
|
|
/*! Creates a predicate matching URL query parameters.
|
|
* \param parameters A QHash of query parameters that need to be present in the URL with defined values.
|
|
* The keys of the hash are the expected parameter names and the corresponding values of the hash are the
|
|
* expected parameter values in the order they appear in the query.
|
|
* \param format QUrl::ComponentFormattingOptions used to convert the query parameter value to a QString.
|
|
* The default is QUrl::PrettyDecoded since it is also the default for QUrlQuery::queryItemValue().
|
|
* \since 0.4.0
|
|
*/
|
|
explicit QueryParameters(
|
|
const MultiValueQueryParameterHash& parameters,
|
|
QUrl::ComponentFormattingOptions format = QUrl::ComponentFormattingOptions(QUrl::PrettyDecoded))
|
|
: Predicate()
|
|
, m_queryParameters(parameters)
|
|
, m_format(format)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
const QUrlQuery query(request.qRequest.url());
|
|
MultiValueQueryParameterHash::const_iterator iter = m_queryParameters.cbegin();
|
|
const MultiValueQueryParameterHash::const_iterator end = m_queryParameters.cend();
|
|
for (; iter != end; ++iter) {
|
|
if (!query.hasQueryItem(iter.key())
|
|
|| query.allQueryItemValues(iter.key(), m_format) != iter.value()) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
//! \endcond
|
|
|
|
MultiValueQueryParameterHash m_queryParameters;
|
|
QUrl::ComponentFormattingOptions m_format;
|
|
};
|
|
|
|
/*! Matches if *all* URL query parameters match one of the given regular expression pairs.
|
|
*
|
|
* This predicates checks all URL query parameters against the given regular expression pairs in the order
|
|
* they are given. If the first regular expression of a pair matches the name of the query parameter, then the
|
|
* second regular expression must match the value of the parameter. If the value does not match or if the
|
|
* parameter name does not match any of the first regular expressions of the pairs, then the predicate does not
|
|
* match. If all query parameter names match one of the first regular expressions and the parameter values match
|
|
* the corresponding second regular expression, then this predicate matches.
|
|
*
|
|
* Note that for parameters with multiple values, all values of the parameter need to match the second regular
|
|
* expression.
|
|
*
|
|
* This predicate can be used to ensure that there are not unexpected query parameters.
|
|
*/
|
|
class QueryParameterTemplates : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching all query parameters against regular expression pairs.
|
|
*
|
|
* \param templates QVector of QRegularExpression pairs. The first regular expressions are matched against
|
|
* the query parameter names and the second regular expressions are matched against the query parameter
|
|
* values. \param format QUrl::ComponentFormattingOptions used to convert the query parameter value to a
|
|
* QString. The default is QUrl::PrettyDecoded since it is also the default for QUrlQuery::queryItemValue().
|
|
*/
|
|
explicit QueryParameterTemplates(
|
|
const RegExPairVector& templates,
|
|
QUrl::ComponentFormattingOptions format = QUrl::ComponentFormattingOptions(QUrl::PrettyDecoded))
|
|
: Predicate()
|
|
, m_templates(templates)
|
|
, m_format(format)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
typedef QList<QPair<QString, QString>> StringPairList;
|
|
|
|
const QUrlQuery query(request.qRequest.url());
|
|
const StringPairList queryParams = query.queryItems(m_format);
|
|
|
|
StringPairList::const_iterator queryParamsIter = queryParams.cbegin();
|
|
const StringPairList::const_iterator queryParamsEnd = queryParams.cend();
|
|
for (; queryParamsIter != queryParamsEnd; ++queryParamsIter) {
|
|
bool matched = false;
|
|
|
|
RegExPairVector::const_iterator templateIter = m_templates.cbegin();
|
|
const RegExPairVector::const_iterator templateEnd = m_templates.cend();
|
|
for (; templateIter != templateEnd; ++templateIter) {
|
|
if (templateIter->first.match(queryParamsIter->first).hasMatch()) {
|
|
matched = templateIter->second.match(queryParamsIter->second).hasMatch();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!matched)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
//! \endcond
|
|
|
|
RegExPairVector m_templates;
|
|
QUrl::ComponentFormattingOptions m_format;
|
|
};
|
|
|
|
/*! Matches if the request body matches a regular expression.
|
|
*
|
|
* To match against the regular expression, the body needs to be converted to a QString.
|
|
* If a \p codec is provided in the constructor, it is used to convert the body.
|
|
* Else, the predicate tries to determine the codec from the [QNetworkRequest::ContentTypeHeader][]:
|
|
* - If the content type header contains codec information using the `"charset:<CODEC>"` format, this codec is
|
|
* used, if supported.
|
|
* - If the codec is not supported, a warning is printed and the predicate falls back to Latin-1.
|
|
* - If the content type header does not contain codec information, the MIME type is investigated.
|
|
* - If the MIME type is known and
|
|
* inherits from `text/plain`, the predicate uses QTextCodec::codecForUtfText() to detect the codec and falls
|
|
* back to UTF-8 if the codec cannot be detected.
|
|
* - In all other cases, including the case that there is no content type header at all and the case that the
|
|
* content is binary, the predicate uses QTextCodec::codecForUtfText() to detect the codec and falls back to
|
|
* Latin-1 if the codec cannot be detected.
|
|
* \note
|
|
* \parblock
|
|
* When trying to match without using the correct codec, (for example, when matching binary content), the
|
|
* regular expression patterns must be aware of the codec mismatch. In such cases, the best approach is to use
|
|
* the numerical value of the encoded character. For example, matching the character "ç" (LATIN SMALL LETTER C
|
|
* WITH CEDILLA) encoded in UTF-8 when the predicate uses Latin-1 encoding would require the pattern \c "ç"
|
|
* assuming the pattern itself is encoded using UTF-8. Since this can lead to mistakes easily, one should rather
|
|
* use the pattern \c "\\xC3\\x83". \endparblock
|
|
*
|
|
* \sa QMimeDatabase
|
|
* [QNetworkRequest::ContentTypeHeader]: http://doc.qt.io/qt-5/qnetworkrequest.html#KnownHeaders-enum
|
|
*/
|
|
class BodyMatching : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching the request body using a regular expression.
|
|
* \param bodyRegEx The regular expression to match against the request body.
|
|
* \param decoder The decoder to be used to convert the body into a QString. If null, the predicate
|
|
* tries to determine the codec based on the [QNetworkRequest::ContentTypeHeader] or based on the
|
|
* request body. The BodyMatching instance does **not** take ownership of the \p decoder.
|
|
* [QNetworkRequest::ContentTypeHeader]: http://doc.qt.io/qt-5/qnetworkrequest.html#KnownHeaders-enum
|
|
*/
|
|
explicit BodyMatching(const QRegularExpression& bodyRegEx, StringDecoder decoder = StringDecoder())
|
|
: Predicate()
|
|
, m_bodyRegEx(bodyRegEx)
|
|
, m_decoder(decoder)
|
|
, m_charsetFieldRegEx(QStringLiteral("charset:(.*)"))
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
if (!m_decoder.isValid())
|
|
determineDecoder(request);
|
|
|
|
const QString decodedBody = m_decoder.decode(request.body);
|
|
|
|
return m_bodyRegEx.match(decodedBody).hasMatch();
|
|
}
|
|
|
|
void determineDecoder(const Request& request) const
|
|
{
|
|
determineDecoderFromContentType(request);
|
|
|
|
if (!m_decoder.isValid())
|
|
determineDecoderFromBody(request.body);
|
|
}
|
|
|
|
void determineDecoderFromContentType(const Request& request) const
|
|
{
|
|
const QString contentTypeHeader =
|
|
request.qRequest.header(QNetworkRequest::ContentTypeHeader).toString();
|
|
if (contentTypeHeader.isEmpty())
|
|
return;
|
|
|
|
QStringList contentTypeFields = contentTypeHeader.split(QChar::fromLatin1(';'));
|
|
const int charsetFieldIndex = contentTypeFields.indexOf(m_charsetFieldRegEx);
|
|
if (charsetFieldIndex >= 0) {
|
|
const QString& charsetField = contentTypeFields.at(charsetFieldIndex);
|
|
const QString charset = HttpUtils::trimmed(m_charsetFieldRegEx.match(charsetField).captured(1));
|
|
determineDecoderFromCharset(charset);
|
|
} else {
|
|
const QMimeType mimeType = QMimeDatabase().mimeTypeForName(contentTypeFields.first());
|
|
if (mimeType.inherits(QStringLiteral("text/plain")))
|
|
determineDecoderFromBody(request.body, QStringLiteral("utf-8"));
|
|
}
|
|
}
|
|
|
|
void determineDecoderFromCharset(const QString& charset) const
|
|
{
|
|
m_decoder.setCodec(charset);
|
|
if (!m_decoder.isValid()) {
|
|
qCWarning(log) << "Unsupported charset:" << charset;
|
|
useFallbackDecoder();
|
|
}
|
|
}
|
|
|
|
void determineDecoderFromBody(const QByteArray& body,
|
|
const QString& fallbackCodec = QStringLiteral("Latin-1")) const
|
|
{
|
|
m_decoder.setCodecFromData(body, fallbackCodec);
|
|
Q_ASSERT(m_decoder.isValid());
|
|
}
|
|
|
|
void useFallbackDecoder() const
|
|
{
|
|
m_decoder.setCodec(QStringLiteral("Latin-1"));
|
|
Q_ASSERT(m_decoder.isValid());
|
|
}
|
|
|
|
QRegularExpression m_bodyRegEx;
|
|
mutable StringDecoder m_decoder;
|
|
QRegularExpression m_charsetFieldRegEx;
|
|
};
|
|
|
|
/*! Match if the request body contains a given snippet.
|
|
*/
|
|
class BodyContaining : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching a snippet in the request body.
|
|
* \param bodySnippet The byte sequence that needs to exist in the request body.
|
|
*/
|
|
explicit BodyContaining(const QByteArray& bodySnippet)
|
|
: Predicate()
|
|
, m_bodySnippet(bodySnippet)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
return request.body.contains(m_bodySnippet);
|
|
}
|
|
//! \endcond
|
|
|
|
QByteArray m_bodySnippet;
|
|
};
|
|
|
|
/*! Matches if the request body equals a given body.
|
|
* \note This predicate does an exact matching so it is stricter than the
|
|
* other body predicates.
|
|
*/
|
|
class Body : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching the request body.
|
|
* \param body The body to be compared to the request body.
|
|
*/
|
|
explicit Body(const QByteArray& body)
|
|
: Predicate()
|
|
, m_body(body)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
return request.body == m_body;
|
|
}
|
|
//! \endcond
|
|
|
|
QByteArray m_body;
|
|
};
|
|
|
|
/*! Matches if the request contains given headers.
|
|
* Note that the request can contain more headers. This predicate just checks that the given headers exist with
|
|
* the given values. \note For this predicate to work correctly, the type of the header field must be registered
|
|
* with qRegisterMetaType() and QMetaType::registerComparators() or QMetaType::registerEqualsComparator(). \sa
|
|
* QNetworkRequest::header()
|
|
*/
|
|
class Headers : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching a set of request headers.
|
|
* \param headers QHash of headers that need to be present in the request
|
|
* with defined values. The keys of the hash are the names of the expected
|
|
* headers and the corresponding values of the hash are the expected values
|
|
* of the headers.
|
|
*/
|
|
explicit Headers(const HeaderHash& headers)
|
|
: Predicate()
|
|
, m_headers(headers)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
HeaderHash::const_iterator iter = m_headers.cbegin();
|
|
const HeaderHash::const_iterator end = m_headers.cend();
|
|
for (; iter != end; ++iter) {
|
|
if (request.qRequest.header(iter.key()) != iter.value())
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
//! \endcond
|
|
|
|
HeaderHash m_headers;
|
|
};
|
|
|
|
/*! Match if the request contains a given header.
|
|
* Note that the request can contain more headers. This predicate just checks that the given header exists with
|
|
* the given value. \note For this predicate to work correctly, the type of the header field must be registered
|
|
* with qRegisterMetaType() and QMetaType::registerComparators() or QMetaType::registerEqualsComparator(). \sa
|
|
* QNetworkRequest::header()
|
|
*/
|
|
class Header : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching a request header.
|
|
* \param header The header that needs to be present in the request.
|
|
* \param value The value that the \p header needs to have.
|
|
*/
|
|
explicit Header(QNetworkRequest::KnownHeaders header, const QVariant& value)
|
|
: Predicate()
|
|
, m_header(header)
|
|
, m_value(value)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
const QVariant headerValue = request.qRequest.header(m_header);
|
|
return headerValue == m_value;
|
|
}
|
|
//! \endcond
|
|
|
|
QNetworkRequest::KnownHeaders m_header;
|
|
QVariant m_value;
|
|
};
|
|
|
|
/*! Matches if a header value matches a regular expression.
|
|
* \note
|
|
* \parblock
|
|
* - The \p header's value is converted to a string using QVariant::toString() to match it against the regular
|
|
* expression.
|
|
* - This predicate does not distinguish between the case that the header has not been set and the case that the
|
|
* header has been set to an empty value. So both cases match if the \p regEx matches empty strings.
|
|
* \endparblock
|
|
* \sa QNetworkRequest::header()
|
|
*/
|
|
class HeaderMatching : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching a header value using a regular expression.
|
|
* \param header The header whose value needs to match.
|
|
* \param regEx The regular expression matched against the \p header's value.
|
|
*/
|
|
explicit HeaderMatching(QNetworkRequest::KnownHeaders header, const QRegularExpression& regEx)
|
|
: Predicate()
|
|
, m_header(header)
|
|
, m_regEx(regEx)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
const QVariant headerValue = request.qRequest.header(m_header);
|
|
return m_regEx.match(headerValue.toString()).hasMatch();
|
|
}
|
|
//! \endcond
|
|
|
|
QNetworkRequest::KnownHeaders m_header;
|
|
QRegularExpression m_regEx;
|
|
};
|
|
|
|
/*! Matches if the request contains given raw headers.
|
|
* Note that the request can contain more headers. This predicate just checks that the given headers exist with
|
|
* the given values. \sa QNetworkRequest::rawHeader()
|
|
*/
|
|
class RawHeaders : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching a set of raw headers.
|
|
* \param rawHeaders QHash of raw headers that need to be present in the request with defined values.
|
|
* The keys of the hash are the names of the expected headers and
|
|
* the values of the hash are the corresponding expected values of the headers.
|
|
*/
|
|
explicit RawHeaders(const RawHeaderHash& rawHeaders)
|
|
: Predicate()
|
|
, m_rawHeaders(rawHeaders)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
RawHeaderHash::const_iterator iter = m_rawHeaders.cbegin();
|
|
const RawHeaderHash::const_iterator end = m_rawHeaders.cend();
|
|
for (; iter != end; ++iter) {
|
|
if (request.qRequest.rawHeader(iter.key()) != iter.value())
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
//! \endcond
|
|
|
|
RawHeaderHash m_rawHeaders;
|
|
};
|
|
|
|
/*! Matches if the request contains a given raw header.
|
|
* Note that the request can contain more headers. This predicate just checks that the given header exists with
|
|
* the given value. \sa QNetworkRequest::rawHeader()
|
|
*/
|
|
class RawHeader : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching a raw request header.
|
|
* \param header The raw header that needs to be present in the request.
|
|
* \param value The value that the \p header needs to have.
|
|
*/
|
|
explicit RawHeader(const QByteArray& header, const QByteArray& value)
|
|
: Predicate()
|
|
, m_header(header)
|
|
, m_value(value)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
return request.qRequest.rawHeader(m_header) == m_value;
|
|
}
|
|
//! \endcond
|
|
|
|
QByteArray m_header;
|
|
QByteArray m_value;
|
|
};
|
|
|
|
/*! Matches if a raw header value matches a regular expression.
|
|
* \note
|
|
* \parblock
|
|
* - The \p header's value is converted to a string using QString::fromUtf8() to match it against the \p regEx.
|
|
* - This predicate does not distinguish between the case that the header has not been set and the case that the
|
|
* header has been set to an empty value. So both cases match if the \p regEx matches empty strings.
|
|
* \endparblock
|
|
* \sa QNetworkRequest::rawHeader()
|
|
*/
|
|
class RawHeaderMatching : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching the value of a raw header using a regular expression.
|
|
* \param header The raw header whose value needs to match.
|
|
* \param regEx The regular expression matched against the \p header's value.
|
|
*/
|
|
explicit RawHeaderMatching(const QByteArray& header, const QRegularExpression& regEx)
|
|
: Predicate()
|
|
, m_header(header)
|
|
, m_regEx(regEx)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
const QString headerValue = QString::fromUtf8(request.qRequest.rawHeader(m_header));
|
|
return m_regEx.match(headerValue).hasMatch();
|
|
}
|
|
//! \endcond
|
|
|
|
QByteArray m_header;
|
|
QRegularExpression m_regEx;
|
|
};
|
|
|
|
/*! Matches if *all* request headers match one of the given regular expression pairs.
|
|
*
|
|
* This predicates checks all defined request headers against the given regular expression pairs in the order
|
|
* they are given. If the first regular expression of a pair matches the name of the header, then the
|
|
* second regular expression must match the value of the header. If the value does not match or if the header
|
|
* name does not match any of the first regular expressions of the pairs, then the predicate does not match.
|
|
* If all header names match one of the first regular expressions and the header values match the
|
|
* corresponding second regular expression, then this predicate matches.
|
|
*
|
|
* This predicate can be used to ensure that there are no unexpected headers.
|
|
*
|
|
* \note \parblock
|
|
* - This predicate also checks the headers defined using QNetworkRequest::setHeader().
|
|
* - Be aware that the Manager might add QNetworkCookies to the [QNetworkRequest::CookieHeader] in case
|
|
* [QNetworkRequest::CookieLoadControlAttribute] is set to [QNetworkRequest::Automatic].
|
|
* \endparblock
|
|
*
|
|
* ## Example ##
|
|
* \code
|
|
* RegExPairVector headerTemplates;
|
|
* headerTemplates.append( qMakePair( QRegularExpression( "^Accept.*" ), QRegularExpression( ".*" ) ) );
|
|
* headerTemplates.append( qMakePair( QRegularExpression( "^Host" ), QRegularExpression( ".*" ) ) );
|
|
* headerTemplates.append( qMakePair( QRegularExpression( "^User-Agent$" ), QRegularExpression( ".*" ) ) );
|
|
*
|
|
* mockNam.whenGet( QUrl( "http://example.com" ) )
|
|
* .has( RawHeaderTemplates( headerTemplates ) )
|
|
* .reply().withStatus( HttpStatus::OK );
|
|
* mockNam.whenGet( QUrl( "http://example.com" ) )
|
|
* .reply().withError( QNetworkReply::UnknownContentError, "Unexpected header" );
|
|
* \endcode
|
|
*
|
|
* [QNetworkRequest::CookieHeader]: http://doc.qt.io/qt-5/qnetworkrequest.html#KnownHeaders-enum
|
|
* [QNetworkRequest::CookieLoadControlAttribute]: http://doc.qt.io/qt-5/qnetworkrequest.html#Attribute-enum
|
|
* [QNetworkRequest::Automatic]: http://doc.qt.io/qt-5/qnetworkrequest.html#LoadControl-enum
|
|
*/
|
|
class RawHeaderTemplates : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching all headers against regular expression pairs.
|
|
*
|
|
* \param templates QVector of QRegularExpression pairs. The first regular expressions are matched against
|
|
* the header names and the second regular expressions are matched against the header values.
|
|
*/
|
|
explicit RawHeaderTemplates(const RegExPairVector& templates)
|
|
: Predicate()
|
|
, m_templates(templates)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
const QList<QByteArray> headerList = request.qRequest.rawHeaderList();
|
|
QList<QByteArray>::const_iterator headerIter = headerList.cbegin();
|
|
const QList<QByteArray>::const_iterator headerEnd = headerList.cend();
|
|
for (; headerIter != headerEnd; ++headerIter) {
|
|
bool matched = false;
|
|
|
|
RegExPairVector::const_iterator templateIter = m_templates.cbegin();
|
|
const RegExPairVector::const_iterator templateEnd = m_templates.cend();
|
|
for (; templateIter != templateEnd; ++templateIter) {
|
|
if (templateIter->first.match(QString::fromUtf8(*headerIter)).hasMatch()) {
|
|
const QByteArray headerValue = request.qRequest.rawHeader(*headerIter);
|
|
|
|
matched = templateIter->second.match(QString::fromUtf8(headerValue)).hasMatch();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!matched)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
//! \endcond
|
|
|
|
RegExPairVector m_templates;
|
|
};
|
|
|
|
/*! Match if the request has a given attribute.
|
|
* Note that the request can have more attributes. This predicate just checks that the given attribute exists
|
|
* with the given value. \note \parblock
|
|
* - This predicate cannot match the default values of the attributes since QNetworkRequest::attribute()
|
|
* does not return the default values. As a workaround, use the \p matchInvalid flag: when you want to match the
|
|
* default value, set \p value to the default value and set \p matchInvalid to \c true. Then the predicate will
|
|
* match either when the attribute has been set to the default value explicitly or when the attribute has not
|
|
* been set at all and therefore falls back to the default value.
|
|
* - Since the attributes are an internal feature of %Qt and are never sent to a server, using this predicate
|
|
* means mocking the behavior of the QNetworkAccessManager instead of the server. \endparblock \sa
|
|
* QNetworkRequest::attribute()
|
|
*/
|
|
class Attribute : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching a request attribute.
|
|
* \param attribute The request attribute whose values is matched by this predicate.
|
|
* \param value The value that the \p attribute needs to have.
|
|
* \param matchInvalid If \c true, this predicate will match if the attribute has not been specified
|
|
* on the request. So the predicate matches if either the attribute has been set to the given \p value
|
|
* or not set at all. If \c false, this predicate will only match if the attribute has been set
|
|
* to the specified \p value explicitly.
|
|
*/
|
|
explicit Attribute(QNetworkRequest::Attribute attribute, const QVariant& value, bool matchInvalid = false)
|
|
: Predicate()
|
|
, m_attribute(attribute)
|
|
, m_value(value)
|
|
, m_matchInvalid(matchInvalid)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
const QVariant attribute = request.qRequest.attribute(m_attribute);
|
|
return (m_matchInvalid && !attribute.isValid()) || attribute == m_value;
|
|
}
|
|
//! \endcond
|
|
|
|
QNetworkRequest::Attribute m_attribute;
|
|
QVariant m_value;
|
|
bool m_matchInvalid;
|
|
};
|
|
|
|
/*! Matches if a attribute value matches a regular expression.
|
|
* \note
|
|
* \parblock
|
|
* - The \p attributes's value is converted to a string using QVariant::toString() to match it against the
|
|
* regular expression.
|
|
* - This predicate does not distinguish between the case that the attribute has not been set and the case that
|
|
* the attribute has been set to an empty value. So both cases match if the \p regEx matches empty strings.
|
|
* - Since the attributes are an internal feature of %Qt and are never sent to a server, using this predicate
|
|
* means mocking the behavior of the QNetworkAccessManager instead of the server. \endparblock \sa
|
|
* QNetworkRequest::attribute()
|
|
*/
|
|
class AttributeMatching : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching an attribute value using a regular expression.
|
|
* \param attribute The attribute whose value needs to match.
|
|
* \param regEx The regular expression matched against the \p attribute's value.
|
|
*/
|
|
explicit AttributeMatching(QNetworkRequest::Attribute attribute, const QRegularExpression& regEx)
|
|
: Predicate()
|
|
, m_attribute(attribute)
|
|
, m_regEx(regEx)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
const QVariant attributeValue = request.qRequest.attribute(m_attribute);
|
|
return m_regEx.match(attributeValue.toString()).hasMatch();
|
|
}
|
|
//! \endcond
|
|
|
|
QNetworkRequest::Attribute m_attribute;
|
|
QRegularExpression m_regEx;
|
|
};
|
|
|
|
/*! Matches if the request contains a specified Authorization header.
|
|
*
|
|
* In case an unsupported authentication method is required, you might use RawHeaderMatching to "manually" match
|
|
* authorized requests.
|
|
*
|
|
* For example to check for a bearer authorization:
|
|
* \code
|
|
* using namespace MockNetworkAccess;
|
|
*
|
|
* Rule authorizedRequestsRule;
|
|
* authorizedRequestsRule.has( Predicates::RawHeaderMatching( HttpUtils::authorizationHeader(),
|
|
* QRegularExpression( "Bearer .*" ) ) ); \endcode \sa RawHeaderMatching
|
|
*/
|
|
class Authorization : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching an authorization using the HTTP Basic authentication scheme with given
|
|
* username and password. \param username The username that must be given. \param password The password that
|
|
* must be given.
|
|
*/
|
|
explicit Authorization(const QString& username, const QString& password)
|
|
: Predicate()
|
|
{
|
|
QAuthenticator authenticator;
|
|
authenticator.setUser(username);
|
|
authenticator.setPassword(password);
|
|
m_authenticators.append(authenticator);
|
|
m_authChallenge.reset(new HttpUtils::Authentication::Basic(QStringLiteral("dummy")));
|
|
}
|
|
|
|
/*! Creates a predicate matching an authorization using the HTTP Basic authentication scheme with
|
|
* a selection of username and password combinations.
|
|
* \param credentials QHash of username and password combinations. The authorization in the request must
|
|
* match one of these \p credentials.
|
|
*/
|
|
explicit Authorization(const QHash<QString, QString>& credentials)
|
|
: Predicate()
|
|
{
|
|
QHash<QString, QString>::const_iterator iter = credentials.cbegin();
|
|
const QHash<QString, QString>::const_iterator end = credentials.cend();
|
|
for (; iter != end; ++iter) {
|
|
QAuthenticator authenticator;
|
|
authenticator.setUser(iter.key());
|
|
authenticator.setPassword(iter.value());
|
|
m_authenticators.append(authenticator);
|
|
}
|
|
m_authChallenge.reset(new HttpUtils::Authentication::Basic(QStringLiteral("dummy")));
|
|
}
|
|
|
|
/*! Creates a predicate matching an authorization which matches a given authentication challenge with
|
|
* credentials defined by a given QAuthenticator.
|
|
* \param authChallenge The authentication challenge which the authorization in the request must match.
|
|
* \param authenticators Allowed username and password combinations. The authorization in the request must
|
|
* match one of these combinations.
|
|
*/
|
|
explicit Authorization(const HttpUtils::Authentication::Challenge::Ptr& authChallenge,
|
|
const QVector<QAuthenticator>& authenticators)
|
|
: Predicate()
|
|
, m_authChallenge(authChallenge)
|
|
, m_authenticators(authenticators)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
QVector<QAuthenticator>::const_iterator iter = m_authenticators.cbegin();
|
|
const QVector<QAuthenticator>::const_iterator end = m_authenticators.cend();
|
|
for (; iter != end; ++iter) {
|
|
if (m_authChallenge->verifyAuthorization(request.qRequest, *iter))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
//! \endcond
|
|
|
|
HttpUtils::Authentication::Challenge::Ptr m_authChallenge;
|
|
QVector<QAuthenticator> m_authenticators;
|
|
};
|
|
|
|
/*! Matches if a request contains a cookie with a given value.
|
|
* Note that the request can contain more cookies. This predicate just checks that the given cookie exists with
|
|
* the given value.
|
|
*
|
|
* \note
|
|
* \parblock
|
|
* - If there is no cookie with the given name, this predicate does not match.
|
|
* - In case there are multiple cookies with the given name, the first one is used and the other ones are
|
|
* ignored. \endparblock
|
|
*
|
|
* \sa [QNetworkRequest::CookieHeader]
|
|
* [QNetworkRequest::CookieHeader]: http://doc.qt.io/qt-5/qnetworkrequest.html#KnownHeaders-enum
|
|
*/
|
|
class Cookie : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching a cookie value.
|
|
* \param cookie The cookie which should exist. Only the QNetworkCookie::name() and QNetworkCookie::value()
|
|
* are used to match. Other properties of the cookie (like QNetworkCookie::domain() or
|
|
* QNetworkCookie::expiryDate()) are ignored.
|
|
*/
|
|
explicit Cookie(const QNetworkCookie& cookie)
|
|
: Predicate()
|
|
, m_cookie(cookie)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
const QList<QNetworkCookie> requestCookies =
|
|
request.qRequest.header(QNetworkRequest::CookieHeader).value<QList<QNetworkCookie>>();
|
|
QList<QNetworkCookie>::const_iterator iter = requestCookies.cbegin();
|
|
const QList<QNetworkCookie>::const_iterator end = requestCookies.cend();
|
|
|
|
for (; iter != end; ++iter) {
|
|
const QNetworkCookie requestCookie = *iter;
|
|
/* We use the first matching cookie and ignore possible other cookies with the same name.
|
|
* RFC 6265 does not define a "correct" way to handle this but this seems to be the common practice.
|
|
* See https://stackoverflow.com/a/24214538/490560
|
|
*/
|
|
if (requestCookie.name() == m_cookie.name())
|
|
return (requestCookie.value() == m_cookie.value());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
//! \endcond
|
|
|
|
QNetworkCookie m_cookie;
|
|
};
|
|
|
|
/*! Matches if a request contains a cookie with a value matching a regular expression.
|
|
* \note
|
|
* \parblock
|
|
* - The cookies's value is converted to a string using QString::fromUtf8() to match it against the \p regEx.
|
|
* - If there is no cookie with the given name, this predicate does not match, no matter what \p regEx is.
|
|
* - If the cookie's value is empty, it is matched against the \p regEx.
|
|
* - In case there are multiple cookies with the given name, the first one is used and the other ones are
|
|
* ignored. \endparblock \sa QNetworkRequest::rawHeader()
|
|
*/
|
|
class CookieMatching : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching the value of a cookie using a regular expression.
|
|
* \param cookieName The name of the cookie whose value needs to match.
|
|
* \param regEx The regular expression matched against the \p header's value.
|
|
*/
|
|
explicit CookieMatching(const QByteArray& cookieName, const QRegularExpression& regEx)
|
|
: Predicate()
|
|
, m_cookieName(cookieName)
|
|
, m_regEx(regEx)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
const QList<QNetworkCookie> cookies =
|
|
request.qRequest.header(QNetworkRequest::CookieHeader).value<QList<QNetworkCookie>>();
|
|
QList<QNetworkCookie>::const_iterator iter = cookies.cbegin();
|
|
const QList<QNetworkCookie>::const_iterator end = cookies.cend();
|
|
for (; iter != end; ++iter) {
|
|
const QByteArray cookieName = iter->name();
|
|
/* We use the first matching cookie and ignore possible other cookies with the same name.
|
|
* RFC 6265 does not define a "correct" way to handle this but this seems to be the common practice.
|
|
* See https://stackoverflow.com/a/24214538/490560
|
|
*/
|
|
if (m_cookieName == cookieName) {
|
|
const QString cookieValue = QString::fromUtf8(iter->value());
|
|
return m_regEx.match(cookieValue).hasMatch();
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
//! \endcond
|
|
|
|
QByteArray m_cookieName;
|
|
QRegularExpression m_regEx;
|
|
};
|
|
|
|
/*! Matches if a request contains a JSON body equal to a given JSON document.
|
|
*
|
|
* If the request body is not a valid JSON document, then this predicate does not
|
|
* match even if the given JSON document is invalid as well.
|
|
*
|
|
* \note This predicate does an exact matching so it is stricter than the
|
|
* other body predicates.
|
|
*
|
|
* \since 0.5.0
|
|
* \sa JsonBodyContaining
|
|
*/
|
|
class JsonBody : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching a JSON body.
|
|
* \param body The body to be compared to the request body.
|
|
*/
|
|
explicit JsonBody(const QJsonDocument& body)
|
|
: Predicate()
|
|
, m_body(body)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
QJsonParseError error;
|
|
const QJsonDocument parsedDoc = QJsonDocument::fromJson(request.body, &error);
|
|
if (error.error != QJsonParseError::NoError)
|
|
return false;
|
|
|
|
return parsedDoc == m_body;
|
|
}
|
|
//! \endcond
|
|
|
|
QJsonDocument m_body;
|
|
};
|
|
|
|
/*! Matches if a request contains a JSON body which contains the properties or entries of a given JSON document.
|
|
*
|
|
* This predicate does a recursive comparison of JSON object properties and array entries.
|
|
* So the predicate matches if the body of a request is a JSON document which contains at least the properties
|
|
* or entries of the JSON document given to the constructor.
|
|
*
|
|
* For example: Given the following JSON document as the request body:
|
|
* \code{.json}
|
|
* {
|
|
* "prop1": "value 1",
|
|
* "prop2": true,
|
|
* "nested": {
|
|
* "sub prop1": "value 2",
|
|
* "sub prop2": 17,
|
|
* "array prop": [
|
|
* "value 3",
|
|
* "value 4",
|
|
* "value 5"
|
|
* ]
|
|
* }
|
|
* }
|
|
* \endcode
|
|
*
|
|
* Then this predicate would match when constructed with the following JSON documents:
|
|
* \code{.json}
|
|
* {
|
|
* "prop1": "value 1",
|
|
* }
|
|
* \endcode
|
|
* \code{.json}
|
|
* {
|
|
* "nested": {
|
|
* "sub prop2": 17
|
|
* }
|
|
* }
|
|
* \endcode
|
|
* \code{.json}
|
|
* {
|
|
* "nested": {
|
|
* "array prop": [
|
|
* "value 4"
|
|
* ]
|
|
* }
|
|
* }
|
|
* \endcode
|
|
*
|
|
* However, it would fail when given the following JSON documents:
|
|
* \code{.json}
|
|
* [
|
|
* "prop1"
|
|
* ]
|
|
* \endcode
|
|
* \code{.json}
|
|
* {
|
|
* "prop2": false,
|
|
* }
|
|
* \endcode
|
|
* \code{.json}
|
|
* {
|
|
* "nested": {
|
|
* "array prop": [
|
|
* "another value"
|
|
* ]
|
|
* }
|
|
* }
|
|
* \endcode
|
|
*
|
|
* \since 0.5.0
|
|
* \sa JsonBody
|
|
*/
|
|
class JsonBodyContaining : public Rule::Predicate
|
|
{
|
|
public:
|
|
/*! Creates a predicate matching parts of a JSON body.
|
|
*
|
|
* \param bodyPart The properties or entries to be expected in the request body.
|
|
* \param ensureArrayOrder If \c true, array entries must appear in the same (relative) order
|
|
* in the request body as in \p bodyPart. If \c false, the order of the array entries does not matter,
|
|
* only the existence of the entries is verified. Note that even if this is \c true, there can still
|
|
* be other entries in the arrays of the request body.
|
|
*/
|
|
explicit JsonBodyContaining(const QJsonDocument& bodyPart, bool ensureArrayOrder = false)
|
|
: Predicate()
|
|
, m_bodyPart(bodyPart)
|
|
, m_ensureArrayOrder(ensureArrayOrder)
|
|
{
|
|
}
|
|
|
|
private:
|
|
//! \cond PRIVATE_IMPLEMENTATION
|
|
virtual bool match(const Request& request) Q_DECL_OVERRIDE
|
|
{
|
|
QJsonParseError error;
|
|
const QJsonDocument parsedDoc = QJsonDocument::fromJson(request.body, &error);
|
|
if (error.error != QJsonParseError::NoError)
|
|
return false;
|
|
|
|
if (m_bodyPart.isArray()) {
|
|
if (!parsedDoc.isArray())
|
|
return false;
|
|
return matchArrays(parsedDoc.array(), m_bodyPart.array());
|
|
}
|
|
if (m_bodyPart.isObject()) {
|
|
if (!parsedDoc.isObject())
|
|
return false;
|
|
return matchObjects(parsedDoc.object(), m_bodyPart.object());
|
|
}
|
|
|
|
// LCOV_EXCL_START
|
|
Q_UNREACHABLE();
|
|
return false;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
//! \endcond
|
|
|
|
bool matchValues(const QJsonValue& value, const QJsonValue& expectedValue)
|
|
{
|
|
if (isSimpleValue(value))
|
|
return value == expectedValue;
|
|
|
|
if (value.isArray()) {
|
|
if (!expectedValue.isArray())
|
|
return false;
|
|
|
|
return matchArrays(value.toArray(), expectedValue.toArray()); // RECURSION !!!
|
|
}
|
|
|
|
if (value.isObject()) {
|
|
if (!expectedValue.isObject())
|
|
return false;
|
|
|
|
return matchObjects(value.toObject(), expectedValue.toObject()); // RECURSION !!!
|
|
}
|
|
|
|
// LCOV_EXCL_START
|
|
Q_UNREACHABLE();
|
|
return false;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
|
|
static bool isSimpleValue(const QJsonValue& value)
|
|
{
|
|
return value.isString() || value.isBool() || value.isDouble() || isNullish(value);
|
|
}
|
|
|
|
static bool isNullish(const QJsonValue& value)
|
|
{
|
|
return value.isNull() || value.isUndefined();
|
|
}
|
|
|
|
bool matchArrays(const QJsonArray& array, const QJsonArray& expectedEntries)
|
|
{
|
|
if (m_ensureArrayOrder)
|
|
return matchArraysEnsureOrder(array, expectedEntries); // RECURSION !!!
|
|
return matchArraysIgnoreOrder(array, expectedEntries); // RECURSION !!!
|
|
}
|
|
|
|
bool matchArraysIgnoreOrder(const QJsonArray& array, QJsonArray expectedEntries)
|
|
{
|
|
auto iter = array.constBegin();
|
|
const auto end = array.constEnd();
|
|
|
|
for (; iter != end; ++iter) {
|
|
auto expectedIter = expectedEntries.begin();
|
|
const auto expectedEnd = expectedEntries.end();
|
|
while (expectedIter != expectedEnd) {
|
|
if (matchValues(*iter, *expectedIter)) // RECURSION !!!
|
|
{
|
|
expectedIter = expectedEntries.erase(expectedIter);
|
|
break;
|
|
}
|
|
|
|
++expectedIter;
|
|
}
|
|
if (expectedEntries.isEmpty())
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool matchArraysEnsureOrder(const QJsonArray& array, QJsonArray expectedEntries)
|
|
{
|
|
auto iter = array.constBegin();
|
|
const auto end = array.constEnd();
|
|
auto expectedIter = expectedEntries.begin();
|
|
|
|
for (; iter != end; ++iter) {
|
|
if (matchValues(*iter, *expectedIter)) // RECURSION !!!
|
|
{
|
|
expectedIter = expectedEntries.erase(expectedIter);
|
|
if (expectedEntries.isEmpty())
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool matchObjects(const QJsonObject& object, const QJsonObject& expectedProps)
|
|
{
|
|
auto iter = expectedProps.constBegin();
|
|
const auto end = expectedProps.constEnd();
|
|
|
|
for (; iter != end; ++iter) {
|
|
if (!object.contains(iter.key())
|
|
|| !matchValues(object.value(iter.key()), iter.value())) // RECURSION !!!
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
QJsonDocument m_bodyPart;
|
|
bool m_ensureArrayOrder;
|
|
};
|
|
|
|
} // namespace Predicates
|
|
|
|
/*! Defines the possible behaviors of the Manager when a request does not match any Rule.
|
|
*
|
|
* By default, the Manager returns a predefined reply for unmatched requests. The reply has set
|
|
* QNetworkReply::ContentNotFoundError and an error message indicating that the request did not
|
|
* match any Rule.
|
|
* The default reply can be modified via Manager::unmatchedRequestBuilder().
|
|
*/
|
|
enum UnmatchedRequestBehavior
|
|
{
|
|
PassThrough, /*!< Unmatched requests are passed through to the next network access manager.
|
|
* \sa Manager::setPassThroughNam()
|
|
* \sa \ref page_passThrough
|
|
*/
|
|
PredefinedReply /*!< The manager will return a predefined reply for unmatched requests.
|
|
* \since 0.8.0 This is the default behavior.
|
|
* \sa Manager::setUnmatchedRequestBuilder()
|
|
*/
|
|
};
|
|
|
|
} // namespace MockNetworkAccess
|
|
|
|
Q_DECLARE_METATYPE(MockNetworkAccess::VersionNumber)
|
|
Q_DECLARE_METATYPE(MockNetworkAccess::Request)
|
|
Q_DECLARE_METATYPE(MockNetworkAccess::Rule::Ptr)
|
|
|
|
namespace MockNetworkAccess
|
|
{
|
|
|
|
/*! Helper class which emits signals for the Manager.
|
|
*
|
|
* Since template classes cannot use the `Q_OBJECT` macro, they cannot define signals or slots.
|
|
* For this reason, this helper class is needed to allow emitting signals from the Manager.
|
|
*
|
|
* To get the signal emitter, call Manager::signalEmitter().
|
|
*
|
|
* \sa Manager::signalEmitter()
|
|
*/
|
|
class SignalEmitter : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
template <class Base> friend class Manager;
|
|
|
|
public:
|
|
/*! Default destructor
|
|
*/
|
|
virtual ~SignalEmitter()
|
|
{
|
|
}
|
|
|
|
private:
|
|
/*! Creates a SignalEmitter object.
|
|
*
|
|
* \note This registers the types Request and Rule::Ptr in the %Qt meta type system
|
|
* using qRegisterMetaType().
|
|
*
|
|
* \param parent Parent QObject.
|
|
*/
|
|
explicit SignalEmitter(QObject* parent = Q_NULLPTR)
|
|
: QObject(parent)
|
|
{
|
|
registerMetaTypes();
|
|
}
|
|
|
|
Q_SIGNALS:
|
|
|
|
/*! Emitted when the Manager receives a request through its public interface (QNetworkAccessManager::get()
|
|
* etc.). \param request The request.
|
|
*/
|
|
void receivedRequest(const MockNetworkAccess::Request& request);
|
|
|
|
/*! Emitted when the Manager handles a request.
|
|
*
|
|
* This signal is emitted for requests received through the public interface (see receivedRequest()) as well as
|
|
* requests created internally by the Manager for example when automatically following redirects or when
|
|
* handling authentication.
|
|
*
|
|
* \param request The request.
|
|
*/
|
|
void handledRequest(const MockNetworkAccess::Request& request);
|
|
|
|
/*! Emitted when a request matches a Rule.
|
|
* \param request The request.
|
|
* \param rule The matched Rule.
|
|
*/
|
|
void matchedRequest(const MockNetworkAccess::Request& request, MockNetworkAccess::Rule::Ptr rule);
|
|
|
|
/*! Emitted when the Manager received a request which did not match any of its Rules.
|
|
* \param request The request.
|
|
*/
|
|
void unmatchedRequest(const MockNetworkAccess::Request& request);
|
|
|
|
/*! Emitted when the Manager passed a request through to the next network access manager.
|
|
* \param request The request.
|
|
* \sa Manager::setPassThroughNam()
|
|
*/
|
|
void passedThrough(const MockNetworkAccess::Request& request);
|
|
|
|
private:
|
|
static void registerMetaTypes()
|
|
{
|
|
static QAtomicInt registered;
|
|
if (registered.testAndSetAcquire(0, 1)) {
|
|
::qRegisterMetaType<Request>();
|
|
::qRegisterMetaType<Rule::Ptr>();
|
|
}
|
|
}
|
|
};
|
|
|
|
/*! \internal Implementation details.
|
|
*/
|
|
namespace detail
|
|
{
|
|
|
|
/*! \internal
|
|
* Updates the state of a QNetworkAccessManager according to reply headers.
|
|
* This includes updating cookies and HSTS entries.
|
|
*/
|
|
class ReplyHeaderHandler : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
ReplyHeaderHandler(QNetworkAccessManager* manager, QObject* parent = Q_NULLPTR)
|
|
: QObject(parent)
|
|
, m_manager(manager)
|
|
{
|
|
}
|
|
|
|
virtual ~ReplyHeaderHandler()
|
|
{
|
|
}
|
|
|
|
public Q_SLOTS:
|
|
void handleReplyHeaders(QNetworkReply* sender = Q_NULLPTR)
|
|
{
|
|
QNetworkReply* reply = getReply(sender);
|
|
|
|
handleKnownHeaders(reply);
|
|
handleRawHeaders(reply);
|
|
}
|
|
|
|
private:
|
|
QNetworkReply* getReply(QNetworkReply* sender)
|
|
{
|
|
if (sender)
|
|
return sender;
|
|
|
|
QNetworkReply* reply = ::qobject_cast<QNetworkReply*>(this->sender());
|
|
Q_ASSERT(reply);
|
|
return reply;
|
|
}
|
|
|
|
void handleKnownHeaders(QNetworkReply* reply)
|
|
{
|
|
handleSetCookieHeader(reply);
|
|
}
|
|
|
|
void handleSetCookieHeader(QNetworkReply* reply)
|
|
{
|
|
QNetworkRequest request = reply->request();
|
|
const bool saveCookies = requestSavesCookies(request);
|
|
|
|
QNetworkCookieJar* cookieJar = m_manager->cookieJar();
|
|
if (saveCookies && cookieJar) {
|
|
const QList<QNetworkCookie> cookies =
|
|
reply->header(QNetworkRequest::SetCookieHeader).value<QList<QNetworkCookie>>();
|
|
if (!cookies.isEmpty())
|
|
cookieJar->setCookiesFromUrl(cookies, reply->url());
|
|
}
|
|
}
|
|
|
|
static bool requestSavesCookies(const QNetworkRequest& request)
|
|
{
|
|
const int defaultValue = static_cast<int>(QNetworkRequest::Automatic);
|
|
const int saveCookiesInt =
|
|
request.attribute(QNetworkRequest::CookieSaveControlAttribute, defaultValue).toInt();
|
|
return static_cast<QNetworkRequest::LoadControl>(saveCookiesInt) == QNetworkRequest::Automatic;
|
|
}
|
|
|
|
void handleRawHeaders(QNetworkReply* reply)
|
|
{
|
|
const QList<QNetworkReply::RawHeaderPair>& rawHeaderPairs = reply->rawHeaderPairs();
|
|
QList<QNetworkReply::RawHeaderPair>::const_iterator headerIter = rawHeaderPairs.cbegin();
|
|
const QList<QNetworkReply::RawHeaderPair>::const_iterator headerEnd = rawHeaderPairs.cend();
|
|
for (; headerIter != headerEnd; ++headerIter) {
|
|
// header field-name is ASCII according to RFC 7230 3.2
|
|
const QByteArray headerName = headerIter->first.toLower();
|
|
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
|
|
const QByteArray stsHeader("strict-transport-security");
|
|
if (headerName == stsHeader) {
|
|
handleStsHeader(headerIter->second, reply);
|
|
}
|
|
#endif // Qt >= 5.9.0
|
|
}
|
|
}
|
|
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
|
|
void handleStsHeader(const QByteArray& headerValue, const QNetworkReply* reply)
|
|
{
|
|
const QStringList stsPolicies = HttpUtils::splitCommaSeparatedList(QString::fromLatin1(headerValue));
|
|
QStringList::const_iterator stsPolicyIter = stsPolicies.constBegin();
|
|
const QStringList::const_iterator stsPolicyEnd = stsPolicies.constEnd();
|
|
for (; stsPolicyIter != stsPolicyEnd; ++stsPolicyIter) {
|
|
/* If the header has an invalid syntax, we ignore it and continue
|
|
* until we find a valid STS policy.
|
|
*/
|
|
if (processStsPolicy(stsPolicyIter->toLatin1(), reply->url()))
|
|
break; // following STS policies are ignored
|
|
continue;
|
|
}
|
|
}
|
|
|
|
bool processStsPolicy(const QByteArray& header, const QUrl& host)
|
|
{
|
|
const QString headerData = QString::fromLatin1(header);
|
|
const QStringList directives = headerData.split(QChar::fromLatin1(';'));
|
|
|
|
QHstsPolicy policy;
|
|
policy.setHost(host.host());
|
|
|
|
QSet<QString> foundDirectives;
|
|
|
|
QStringList::const_iterator directiveIter = directives.cbegin();
|
|
const QStringList::const_iterator directiveEnd = directives.cend();
|
|
for (; directiveIter != directiveEnd; ++directiveIter) {
|
|
const QString cleanDirective = HttpUtils::whiteSpaceCleaned(*directiveIter);
|
|
const QPair<QString, QString> directiveSplit = splitStsDirective(cleanDirective);
|
|
const QString directiveName = directiveSplit.first;
|
|
const QString directiveValue = directiveSplit.second;
|
|
|
|
if (foundDirectives.contains(directiveName))
|
|
return false; // Invalid header: duplicate directive
|
|
foundDirectives.insert(directiveName);
|
|
|
|
if (!processStsDirective(policy, directiveName, directiveValue))
|
|
return false;
|
|
}
|
|
|
|
if (!foundDirectives.contains(maxAgeDirectiveName()))
|
|
return false; // Invalid header: missing required max-age directive
|
|
|
|
m_manager->addStrictTransportSecurityHosts(QVector<QHstsPolicy>() << policy);
|
|
return true;
|
|
}
|
|
|
|
static QLatin1String maxAgeDirectiveName()
|
|
{
|
|
return QLatin1String("max-age");
|
|
}
|
|
|
|
static QPair<QString, QString> splitStsDirective(const QString& directive)
|
|
{
|
|
const QRegularExpression basicDirectiveRegEx(QStringLiteral("^([^=]*)=?(.*)$"));
|
|
|
|
QRegularExpressionMatch match;
|
|
|
|
match = basicDirectiveRegEx.match(directive);
|
|
// This should be impossible since basicDirectiveRegEx matches everything
|
|
Q_ASSERT_X(match.hasMatch(), Q_FUNC_INFO, "Could not parse directive.");
|
|
|
|
const QString directiveName = HttpUtils::whiteSpaceCleaned(match.captured(1)).toLower();
|
|
const QString rawDirectiveValue = HttpUtils::whiteSpaceCleaned(match.captured(2));
|
|
const QString directiveValue = HttpUtils::isValidToken(rawDirectiveValue)
|
|
? rawDirectiveValue
|
|
: HttpUtils::unquoteString(rawDirectiveValue);
|
|
|
|
return ::qMakePair(directiveName, directiveValue);
|
|
}
|
|
|
|
static bool
|
|
processStsDirective(QHstsPolicy& policy, const QString& directiveName, const QString& directiveValue)
|
|
{
|
|
if (directiveName == maxAgeDirectiveName()) {
|
|
return processStsMaxAgeDirective(policy, directiveValue);
|
|
}
|
|
|
|
if (directiveName == QLatin1String("includesubdomains")) {
|
|
policy.setIncludesSubDomains(true);
|
|
return true;
|
|
}
|
|
|
|
// else we check if the directive is legal at all
|
|
if (!HttpUtils::isValidToken(directiveName))
|
|
return false; // Invalid header: illegal directive name
|
|
|
|
if (!HttpUtils::isValidToken(directiveValue) && !HttpUtils::isValidQuotedString(directiveValue))
|
|
return false; // Invalid header: illegal directive value
|
|
|
|
// Directive seems legal but simply unknown. So we ignore it.
|
|
return true;
|
|
}
|
|
|
|
static bool processStsMaxAgeDirective(QHstsPolicy& policy, const QString& directiveValue)
|
|
{
|
|
const QRegularExpression maxAgeValueRegEx(QStringLiteral("\\d+"));
|
|
|
|
const QRegularExpressionMatch match = maxAgeValueRegEx.match(directiveValue);
|
|
if (!match.hasMatch())
|
|
return false; // Invalid header: incorrect max-age value
|
|
const qint64 maxAge = match.captured(0).toLongLong();
|
|
policy.setExpiry(QDateTime::currentDateTimeUtc().addSecs(maxAge));
|
|
return true;
|
|
}
|
|
#endif // Qt >= 5.9.0
|
|
|
|
QPointer<QNetworkAccessManager> m_manager;
|
|
};
|
|
|
|
} // namespace detail
|
|
|
|
/*! Determines the behavior of the %Qt version in use.
|
|
* This is also the default behavior of Manager objects if not overridden using Manager::setBehaviorFlags().
|
|
* \return The BehaviorFlags matching the behavior of the %Qt version used at runtime.
|
|
* \sa [qVersion()](https://doc.qt.io/qt-5/qtglobal.html#qVersion)
|
|
* \sa BehaviorFlag
|
|
* \since 0.3.0
|
|
*/
|
|
inline BehaviorFlags getDefaultBehaviorFlags()
|
|
{
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 2, 0)
|
|
#error MockNetworkAccessManager requires Qt 5.2.0 or later
|
|
#endif
|
|
const char* qtVersion = ::qVersion();
|
|
const VersionNumber qtVersionInUse = VersionNumber::fromString(QString::fromLatin1(qtVersion));
|
|
|
|
const VersionNumber qt5_6_0(5, 6, 0);
|
|
|
|
if (qtVersionInUse >= qt5_6_0)
|
|
return Behavior_Qt_5_6_0;
|
|
else
|
|
return Behavior_Qt_5_2_0;
|
|
}
|
|
|
|
/*! Mixin class to mock network replies from QNetworkAccessManager.
|
|
* %Manager mocks the QNetworkReplys instead of sending the requests over the network.
|
|
* %Manager is a mixin class meaning it can be used "on top" of every class inheriting publicly from
|
|
* QNetworkAccessManager.
|
|
*
|
|
* \tparam Base QNetworkAccessManager or a class publicly derived from QNetworkAccessManager.
|
|
*
|
|
*
|
|
* ## Configuration ##
|
|
* To define which and how requests are answered with mocked replies, the %Manager is configured using
|
|
* \link Rule Rules\endlink:
|
|
* Whenever the %Manager is handed over a request, it matches the request against its rules one after the other.\n
|
|
* - If a rule reports a match for the request, the %Manager requests the rule to create a reply for that request.\n
|
|
* - If the rule creates a reply, then this reply is returned by the %Manager.\n
|
|
* - If the rule does not create a reply, the %Manager continues matching the request against the remaining
|
|
* rules.\n
|
|
* - If no rule matches the request or no rule created a reply, the "unmatched request behavior" steps in.\n
|
|
* This means either:
|
|
* 1. the request is passed through to the next network access manager (see setPassThroughNam()) and the
|
|
* corresponding QNetworkReply is returned.
|
|
* 2. a predefined reply is returned (see unmatchedRequestBuilder()).
|
|
*
|
|
* The latter is the default behavior. For more details see \ref UnmatchedRequestBehavior.
|
|
*
|
|
* To define which requests match a rule, the Rule object is configured by adding predicates.
|
|
*
|
|
* To define the properties of the created replies, the %Rule object exposes a MockReplyBuilder via the
|
|
* Rule::reply() method.
|
|
*
|
|
* To add a rule to the %Manager, you can either:
|
|
* - create a %Rule object, configure it and add it using addRule().
|
|
* - use the convenience methods whenGet(), whenPost(), when() etc. and configure the returned %Rule objects.
|
|
*
|
|
* To retrieve or remove Rules or change their order, use the methods rules() and setRules().
|
|
*
|
|
*
|
|
* ### Example ###
|
|
*
|
|
* \code
|
|
* using namespace MockNetworkAccess;
|
|
* using namespace MockNetworkAccess::Predicates;
|
|
*
|
|
* // Create the Manager
|
|
* Manager< QNetworkAccessManager > mockNam;
|
|
*
|
|
* // Simple configuration
|
|
* mockNam.whenGet( QRegularExpression( "https?://example.com/data/.*" ) )
|
|
* .reply().withBody( QJsonDocument::fromJson( "{ \"id\": 736184, \"data\": \"Hello World!\" }" );
|
|
*
|
|
* // More complex configuration
|
|
* Rule::Ptr accountInfoRequest( new Rule );
|
|
* accountInfoRequest->has( Verb( QNetworkAccessManager::GetOperation ) )
|
|
* .has( UrlMatching( QRegularExpression( "https?://example.com/accountInfo/.*" ) ) );
|
|
*
|
|
* Rule::Ptr authorizedAccountInfoRequest( accountInfoRequest->clone() );
|
|
* authorizedAccountInfoRequest->has( RawHeaderMatching( HttpUtils::authorizationHeader(), QRegularExpression(
|
|
* "Bearer: .*" ) ) ) .reply().withBody( QJsonDocument::fromJson( "{ \"name\": \"John Doe\", \"email\":
|
|
* \"john.doe@example.com\" }" ) );
|
|
*
|
|
* Rule::Ptr unauthorizedAccountInfoRequest( accountInfoRequest->clone() );
|
|
* unauthorizedAccountInfoRequest->reply().withStatus( 401 );
|
|
*
|
|
* // The order is important here since the
|
|
* // first matching rule will create the reply.
|
|
* mockNam.add( authorizedAccountInfoRequest );
|
|
* mockNam.add( unauthorizedAccountInfoRequest );
|
|
*
|
|
* // All other requests
|
|
* mockNam.unmatchedRequestBuilder().withStatus( 404 );
|
|
*
|
|
* // Use the Manager
|
|
* MyNetworkClient myNetworkClient;
|
|
* myNetworkClient.setNetworkManager( &mockNam );
|
|
* myNetworkClient.run();
|
|
* \endcode
|
|
*
|
|
* ### Signals ###
|
|
* Since the Manager is a template class, it cannot define signals due to limitations of %Qt's meta object compiler
|
|
* (moc).
|
|
*
|
|
* To solve this, the Manager provides a SignalEmitter (see signalEmitter()) which emits the signals on behalf of
|
|
* the Manager.
|
|
*
|
|
* [QNetworkRequest::UserVerifiedRedirectPolicy]: http://doc.qt.io/qt-5/qnetworkrequest.html#RedirectPolicy-enum
|
|
* [QNetworkRequest::Attributes]: http://doc.qt.io/qt-5/qnetworkrequest.html#Attribute-enum
|
|
*
|
|
*
|
|
* ## Handling of non-HTTP Protocols ##
|
|
* The Manager also supports FTP, `data:`, `file:` and `qrc:` requests. However, for `data:`, `file:` and `qrc:`
|
|
* requests the Manager behaves differently as for HTTP or FTP requests.
|
|
*
|
|
* ### `data:` Requests ###
|
|
* `data:` requests are always forwarded to the \p Base network access manager. That's the easiest way to implement
|
|
* the handling of such requests and since they are never sent to the network it does not make sense to allow any
|
|
* kind of reply mocking there. This means that requests with a `data:` URL are never matched against any rule and
|
|
* these requests are never contained in the matchedRequests(), unmatchedRequests() or passedThroughRequests().
|
|
* However, they are contained in the receivedRequests() and handledRequests().
|
|
*
|
|
* ### `file:` and `qrc:` Requests ###
|
|
* Requests with a `file:` URL only support the \link QNetworkAccessManager::get() GET \endlink and
|
|
* \link QNetworkAccessManager::put() PUT \endlink operations. Requests with a `qrc:` URL only support the
|
|
* \link QNetworkAccessManager::get() GET \endlink operation. All other operations will result in a reply
|
|
* with an QNetworkReply::ProtocolUnknownError.
|
|
*
|
|
* If you want to mock a successful `PUT` operation of a `file:` request, you should configure the rule to reply
|
|
* with QNetworkReply::NoError. It is necessary to call one of the `with*()` methods of the MockReplyBuilder for the
|
|
* Rule to be considered valid by the Manager. And setting `withError( QNetworkReply::NoError )` is the only
|
|
* configuration that is applicable for a successful `PUT` operation for a `file:` request. For example:
|
|
*
|
|
* \code
|
|
* using namespace MockNetworkAccess;
|
|
*
|
|
* Manager< QNetworkAccessManager > mnam;
|
|
* mnam.whenPut( QUrl( "file:///path/to/file" ) ).reply().withError( QNetworkReply::NoError );
|
|
* \endcode
|
|
*
|
|
*
|
|
* ## Limitations ##
|
|
* The Manager currently has a few limitations:
|
|
* - When a request with automatic redirect following is passed through and gets redirected,
|
|
* the rules of the initial Manager are not applied to the redirect
|
|
* (see \ref page_passThrough_redirects and issue \issue{15}).
|
|
* - When a request is redirected and then passed through to a separate QNetworkAccessManager
|
|
* (see setPassThroughNam()), the QNetworkReply::metaDataChanged() and
|
|
* QNetworkReply::redirected() signals of the mocked redirections are emitted out of order (namely after all other
|
|
* signals).
|
|
* - The mocked replies do not emit the implementation specific signals of a real HTTP based QNetworkReply
|
|
* (that is the signals of QNetworkReplyHttpImpl).
|
|
* - Out of the box, only HTTP Basic authentication is supported. However, this should not be a problem in most
|
|
* cases since the handling of authentication is normally done internally between the `MockNetworkAccess::Manager`
|
|
* and the `MockReply`.\n This is only a limitation if you manually create `Authorization` headers and have to rely
|
|
* on HTTP Digest or NTLM authentication.\n Note that it is still possible to work with any authentication method by
|
|
* matching the `Authorization` header manually (for example using Predicates::RawHeaderMatching) or by implementing
|
|
* a \link Rule::Predicate custom predicate\endlink.
|
|
* - The QAuthenticator passed in the `QNetworkAccessManager::authenticationRequired()` signal does not provide the
|
|
* `realm` parameter via the `QAuthenticator::realm()` method in %Qt before 5.4.0 but only as option with the key
|
|
* `realm` (for example, via `authenticator->option("realm")`).
|
|
* - Proxy authentication is not supported at the moment.
|
|
* - [QNetworkRequest::UserVerifiedRedirectPolicy] is not supported at the moment.
|
|
* - The error messages of the replies (QNetworkReply::errorString()) may be different from the ones of real
|
|
* QNetworkReply objects.
|
|
* - QNetworkReply::setReadBufferSize() is ignored at the moment.
|
|
*
|
|
*
|
|
* Some of these limitations might be removed in future versions. Feel free to create a feature (or merge) request
|
|
* if you hit one these limitations.
|
|
*
|
|
* Additionally, the Manager supports only selected [QNetworkRequest::Attributes].
|
|
* The following attributes are supported:
|
|
* - QNetworkRequest::HttpStatusCodeAttribute
|
|
* - QNetworkRequest::HttpReasonPhraseAttribute
|
|
* - QNetworkRequest::RedirectionTargetAttribute
|
|
* - QNetworkRequest::ConnectionEncryptedAttribute
|
|
* - QNetworkRequest::CustomVerbAttribute
|
|
* - QNetworkRequest::CookieLoadControlAttribute
|
|
* - QNetworkRequest::CookieSaveControlAttribute
|
|
* - QNetworkRequest::FollowRedirectsAttribute
|
|
* - QNetworkRequest::OriginalContentLengthAttribute
|
|
* - QNetworkRequest::RedirectPolicyAttribute
|
|
*
|
|
* All other attributes are ignored when specified on a QNetworkRequest and are not set when returning a MockReply.
|
|
* However, if desired, the attributes can be matched on a request using Predicates::Attribute or
|
|
* Predicates::AttributeMatching and can be set on a MockReply using MockReplyBuilder::withAttribute().
|
|
*
|
|
* \note
|
|
* \parblock
|
|
* At the moment, the Manager does not handle large request bodies well since it reads them into
|
|
* memory completely to be able to provide them to all the Rule objects.
|
|
*
|
|
* With setInspectBody(), you can disable this if you need to use the Manager with large request
|
|
* bodies and you do not need to match against the body.
|
|
* \endparblock
|
|
*
|
|
*/
|
|
template <class Base> class Manager : public Base
|
|
{
|
|
// cannot use Q_OBJECT with template class
|
|
public:
|
|
/*! Creates a Manager.
|
|
* \param parent Parent QObject.
|
|
*/
|
|
explicit Manager(QObject* parent = Q_NULLPTR)
|
|
: Base(parent)
|
|
, m_inspectBody(true)
|
|
, m_behaviorFlags(getDefaultBehaviorFlags())
|
|
, m_passThroughNam(Q_NULLPTR)
|
|
, m_signalEmitter(Q_NULLPTR)
|
|
, m_unmatchedRequestBehavior(PredefinedReply)
|
|
, m_replyHeaderHandler(new detail::ReplyHeaderHandler(this))
|
|
{
|
|
setupDefaultReplyBuilder();
|
|
}
|
|
|
|
/*! Default destructor */
|
|
virtual ~Manager()
|
|
{
|
|
}
|
|
|
|
/*! Defines whether the message body of requests should be used to match requests.
|
|
* By default, the Manager reads the complete request body into memory to match it against the Rules.
|
|
* Setting \p inspectBody to \c false prevents that the request body is read into memory.
|
|
* However, the matching is then done using a null QByteArray() as request body. So Rules with body predicates
|
|
* will not match unless they match an empty body. \param inspectBody If \c true (the default), the request body
|
|
* will be read and matched against the predicates of the Rules. If \c false, the request body will not be read
|
|
* by the Manager but a null QByteArray() will be used instead.
|
|
*/
|
|
void setInspectBody(bool inspectBody)
|
|
{
|
|
m_inspectBody = inspectBody;
|
|
}
|
|
|
|
/*! \return The behavior flags active on this Manager.
|
|
*/
|
|
BehaviorFlags behaviorFlags() const
|
|
{
|
|
return m_behaviorFlags;
|
|
}
|
|
|
|
/*! Tunes the behavior of this Manager.
|
|
*
|
|
* \param behaviorFlags Combination of BehaviorFlags to define some details of this Manager's behavior.
|
|
* \sa BehaviorFlag
|
|
*/
|
|
void setBehaviorFlags(BehaviorFlags behaviorFlags)
|
|
{
|
|
m_behaviorFlags = behaviorFlags;
|
|
}
|
|
|
|
/*! Defines how the Manager handles requests that do not match any Rule.
|
|
*
|
|
* \param unmatchedRequestBehavior An UnmatchedRequestBehavior flag to define the new behavior.
|
|
*
|
|
* \sa unmatchedRequestBehavior()
|
|
*/
|
|
void setUnmatchedRequestBehavior(UnmatchedRequestBehavior unmatchedRequestBehavior)
|
|
{
|
|
m_unmatchedRequestBehavior = unmatchedRequestBehavior;
|
|
}
|
|
|
|
/*! \return How the Manager handles unmatched requests.
|
|
*
|
|
* \sa setUnmatchedRequestBehavior()
|
|
*/
|
|
UnmatchedRequestBehavior unmatchedRequestBehavior() const
|
|
{
|
|
return m_unmatchedRequestBehavior;
|
|
}
|
|
|
|
/*! Defines a reply builder being used to create replies for requests that do not match any Rule in the Manager.
|
|
*
|
|
* \note This builder is only used when unmatchedRequestBehavior() is PredefinedReply.
|
|
*
|
|
* \param builder The MockReplyBuilder creating the replies for unmatched requests.
|
|
* \sa setUnmatchedRequestBehavior()
|
|
*/
|
|
void setUnmatchedRequestBuilder(const MockReplyBuilder& builder)
|
|
{
|
|
m_unmatchedRequestBuilder = builder;
|
|
}
|
|
|
|
/*! \return The reply builder being used to create replies for requests that do not match any Rule in the
|
|
* Manager.
|
|
*
|
|
* \note This builder is only used when unmatchedRequestBehavior() is PredefinedReply.
|
|
*
|
|
* \sa setUnmatchedRequestBuilder()
|
|
* \sa setUnmatchedRequestBehavior()
|
|
*/
|
|
MockReplyBuilder& unmatchedRequestBuilder()
|
|
{
|
|
return m_unmatchedRequestBuilder;
|
|
}
|
|
|
|
/*! Defines the QNetworkAccessManager to be used in case requests should be passes through to the network.
|
|
* By default, the \p Base class of this Manager is used.
|
|
* \param passThroughNam The network access manager to be used to pass requests through. If this is a null
|
|
* pointer, the \p Base class of this Manager is used. \note This could also be another
|
|
* MockNetworkAccess::Manager. This allows building up a hierarchy of Managers. \sa
|
|
* setUnmatchedRequestBehavior() \sa Rule::passThrough() \sa \ref page_passThrough
|
|
*/
|
|
void setPassThroughNam(QNetworkAccessManager* passThroughNam)
|
|
{
|
|
m_passThroughNam = passThroughNam;
|
|
}
|
|
|
|
/*! \return The network access manager to which requests are passed through or a \c Q_NULLPTR if the requests
|
|
* are passed through to the \p Base class of this Manager.
|
|
*/
|
|
QNetworkAccessManager* passThroughNam() const
|
|
{
|
|
return m_passThroughNam;
|
|
}
|
|
|
|
/*! \return The Rules of this Manager.
|
|
*/
|
|
QVector<Rule::Ptr> rules() const
|
|
{
|
|
return m_rules;
|
|
}
|
|
|
|
/*! Sets the Rules for this Manager.
|
|
* This will remove all previous Rules.
|
|
* \param rules the new rules for this Manager.
|
|
*/
|
|
void setRules(const QVector<Rule::Ptr>& rules)
|
|
{
|
|
m_rules = rules;
|
|
}
|
|
|
|
/*! Adds a Rule to this Manager.
|
|
* The rule is appended to the existing list of Rules.
|
|
* \param rule A QSharedPointer to the Rule to be added to this Manager.
|
|
*/
|
|
void addRule(const Rule::Ptr& rule)
|
|
{
|
|
m_rules.append(rule);
|
|
}
|
|
|
|
/*! Creates a clone of a Rule and adds it to this Manager.
|
|
* The clone of the rule is appended to the existing list of Rules.
|
|
* \param rule The Rule to be added to this Manager.
|
|
* \return A reference to the clone.
|
|
* \sa Rule::clone()
|
|
*/
|
|
Rule& addRule(const Rule& rule)
|
|
{
|
|
Rule::Ptr newRule(rule.clone());
|
|
m_rules.append(newRule);
|
|
return *newRule;
|
|
}
|
|
|
|
/*! Creates and adds a Rule which matches \c GET requests with a URL matching a regular expression.
|
|
* \param urlRegEx The regular expression matched against the request's URL.
|
|
* \return A reference to the created Rule.
|
|
*/
|
|
Rule& whenGet(const QRegularExpression& urlRegEx)
|
|
{
|
|
return when(QNetworkAccessManager::GetOperation, urlRegEx);
|
|
}
|
|
|
|
/*! Creates and adds a Rule which matches \c GET requests with a given URL.
|
|
* \param url The URL matched against the request's URL.
|
|
* \return A reference to the created Rule.
|
|
*/
|
|
Rule& whenGet(const QUrl& url)
|
|
{
|
|
return when(QNetworkAccessManager::GetOperation, url);
|
|
}
|
|
|
|
/*! Creates and adds a Rule which matches \c POST requests with a URL matching a regular expression.
|
|
* \param urlRegEx The regular expression matched against the request's URL.
|
|
* \return A reference to the created Rule.
|
|
*/
|
|
Rule& whenPost(const QRegularExpression& urlRegEx)
|
|
{
|
|
return when(QNetworkAccessManager::PostOperation, urlRegEx);
|
|
}
|
|
|
|
/*! Creates and adds a Rule which matches \c POST requests with a given URL.
|
|
* \param url The URL matched against the request's URL.
|
|
* \return A reference to the created Rule.
|
|
*/
|
|
Rule& whenPost(const QUrl& url)
|
|
{
|
|
return when(QNetworkAccessManager::PostOperation, url);
|
|
}
|
|
|
|
/*! Creates and adds a Rule which matches \c PUT requests with a URL matching a regular expression.
|
|
* \param urlRegEx The regular expression matched against the request's URL.
|
|
* \return A reference to the created Rule.
|
|
*/
|
|
Rule& whenPut(const QRegularExpression& urlRegEx)
|
|
{
|
|
return when(QNetworkAccessManager::PutOperation, urlRegEx);
|
|
}
|
|
|
|
/*! Creates and adds a Rule which matches \c PUT requests with a given URL.
|
|
* \param url The URL matched against the request's URL.
|
|
* \return A reference to the created Rule.
|
|
*/
|
|
Rule& whenPut(const QUrl& url)
|
|
{
|
|
return when(QNetworkAccessManager::PutOperation, url);
|
|
}
|
|
|
|
/*! Creates and adds a Rule which matches \c DELETE requests with a URL matching a regular expression.
|
|
* \param urlRegEx The regular expression matched against the request's URL.
|
|
* \return A reference to the created Rule.
|
|
*/
|
|
Rule& whenDelete(const QRegularExpression& urlRegEx)
|
|
{
|
|
return when(QNetworkAccessManager::DeleteOperation, urlRegEx);
|
|
}
|
|
|
|
/*! Creates and adds a Rule which matches \c DELETE requests with a given URL.
|
|
* \param url The URL matched against the request's URL.
|
|
* \return A reference to the created Rule.
|
|
*/
|
|
Rule& whenDelete(const QUrl& url)
|
|
{
|
|
return when(QNetworkAccessManager::DeleteOperation, url);
|
|
}
|
|
|
|
/*! Creates and adds a Rule which matches \c HEAD requests with a URL matching a regular expression.
|
|
* \param urlRegEx The regular expression matched against the request's URL.
|
|
* \return A reference to the created Rule.
|
|
*/
|
|
Rule& whenHead(const QRegularExpression& urlRegEx)
|
|
{
|
|
return when(QNetworkAccessManager::HeadOperation, urlRegEx);
|
|
}
|
|
|
|
/*! Creates and adds a Rule which matches \c HEAD requests with a given URL.
|
|
* \param url The URL matched against the request's URL.
|
|
* \return A reference to the created Rule.
|
|
*/
|
|
Rule& whenHead(const QUrl& url)
|
|
{
|
|
return when(QNetworkAccessManager::HeadOperation, url);
|
|
}
|
|
|
|
/*! Creates and adds a Rule which matches requests with a given HTTP verb and a URL matching a regular
|
|
* expression. \param operation The HTTP verb which the request needs to match. \param urlRegEx The regular
|
|
* expression matched against the request's URL. \param customVerb The HTTP verb in case \p operation is
|
|
* QNetworkAccessManager::CustomOperation. Else this parameter is ignored. \return A reference to the created
|
|
* Rule.
|
|
*/
|
|
Rule& when(QNetworkAccessManager::Operation operation,
|
|
const QRegularExpression& urlRegEx,
|
|
const QByteArray& customVerb = QByteArray())
|
|
{
|
|
using namespace Predicates;
|
|
Rule::Ptr rule(new Rule());
|
|
rule->has(Verb(operation, customVerb));
|
|
rule->has(UrlMatching(urlRegEx));
|
|
m_rules.append(rule);
|
|
return *rule;
|
|
}
|
|
|
|
/*! Creates and adds a Rule which matches requests with a given HTTP verb and a given URL.
|
|
* \param operation The HTTP verb which the request needs to match.
|
|
* \param url The URL matched against the request's URL.
|
|
* \param customVerb The HTTP verb in case \p operation is QNetworkAccessManager::CustomOperation. Else this
|
|
* parameter is ignored.
|
|
* \return A reference to the created Rule.
|
|
*/
|
|
Rule&
|
|
when(QNetworkAccessManager::Operation operation, const QUrl& url, const QByteArray& customVerb = QByteArray())
|
|
{
|
|
using namespace Predicates;
|
|
Rule::Ptr rule(new Rule());
|
|
rule->has(Verb(operation, customVerb));
|
|
rule->has(Url(url));
|
|
m_rules.append(rule);
|
|
return *rule;
|
|
}
|
|
|
|
/*! Provides access to signals of the Manager.
|
|
*
|
|
* \return A SignalEmitter object which emits signals on behalf of the Manager.
|
|
* The ownership of the SignalEmitter stays with the Manager. The caller must not delete it.
|
|
*
|
|
* \sa SignalEmitter
|
|
*/
|
|
SignalEmitter* signalEmitter() const
|
|
{
|
|
if (!m_signalEmitter)
|
|
m_signalEmitter.reset(new SignalEmitter());
|
|
return m_signalEmitter.get();
|
|
}
|
|
|
|
/*! \return A vector of all requests which this Manager received through its public interface.
|
|
*/
|
|
QVector<Request> receivedRequests() const
|
|
{
|
|
return m_receivedRequests;
|
|
}
|
|
|
|
/*! Returns all requests which were handled by this Manager.
|
|
*
|
|
* This includes the requests received through the public interface (see receivedRequests()) as well as requests
|
|
* created internally by the Manager for example when automatically following redirects or when handling
|
|
* authentication.
|
|
*
|
|
* \return A vector of all requests handled by this Manager.
|
|
*/
|
|
QVector<Request> handledRequests() const
|
|
{
|
|
return m_handledRequests;
|
|
}
|
|
|
|
/*! \return A vector of all requests which matched a Rule.
|
|
*/
|
|
QVector<Request> matchedRequests() const
|
|
{
|
|
return m_matchedRequests;
|
|
}
|
|
|
|
/*! \return A vector of all requests which did not match any Rule.
|
|
*/
|
|
QVector<Request> unmatchedRequests() const
|
|
{
|
|
return m_unmatchedRequests;
|
|
}
|
|
|
|
/*! \return A vector of all requests which where passed through to the next (real) network access manager.
|
|
* \sa setPassThroughNam()
|
|
*/
|
|
QVector<Request> passedThroughRequests() const
|
|
{
|
|
return m_passedThroughRequests;
|
|
}
|
|
|
|
protected:
|
|
/*! Implements the creation of mocked replies.
|
|
*
|
|
* \param operation The HTTP verb of the operation.
|
|
* \param origRequest The QNetworkRequest object.
|
|
* \param body Optional request body.
|
|
* \return A pointer to a QNetworkReply object. The caller takes ownership of the returned reply object. The
|
|
* reply can either be a real QNetworkReply or a mocked reply. In case of a mocked reply, it is an instance of
|
|
* MockReply.
|
|
*
|
|
* \sa QNetworkAccessManager::createRequest()
|
|
*/
|
|
virtual QNetworkReply* createRequest(QNetworkAccessManager::Operation operation,
|
|
const QNetworkRequest& origRequest,
|
|
QIODevice* body) Q_DECL_OVERRIDE;
|
|
|
|
private:
|
|
void setupDefaultReplyBuilder()
|
|
{
|
|
m_unmatchedRequestBuilder.withError(
|
|
QNetworkReply::ContentNotFoundError,
|
|
QStringLiteral("MockNetworkAccessManager: Request did not match any rule"));
|
|
}
|
|
|
|
QNetworkRequest prepareRequest(const QNetworkRequest& origRequest);
|
|
QNetworkReply* handleRequest(const Request& request);
|
|
QIODevice* createIODevice(const QByteArray& data) const;
|
|
QNetworkReply* passThrough(const Request& request, QNetworkAccessManager* overridePassThroughNam = Q_NULLPTR);
|
|
QNetworkReply* authenticateRequest(MockReply* unauthedReply, const Request& unauthedReq);
|
|
QAuthenticator getAuthenticator(MockReply* unauthedReply,
|
|
const Request& unauthedReq,
|
|
const HttpUtils::Authentication::Challenge::Ptr& authChallenge);
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
|
|
QNetworkReply* followRedirect(MockReply* prevReply, const Request& prevReq);
|
|
#endif // Qt >= 5.6.0
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
|
|
bool applyRedirectPolicy(QNetworkRequest::RedirectPolicy policy,
|
|
MockReply* prevReply,
|
|
const QNetworkRequest& prevRequest,
|
|
const QUrl& redirectTarget);
|
|
void prepareHstsHash();
|
|
bool elevateHstsUrl(const QUrl& url);
|
|
#endif // Qt >= 5.9.0
|
|
QNetworkReply* createDataUrlReply(const Request& request);
|
|
void prepareReply(MockReply* reply, const Request& request) const;
|
|
void finishReply(QNetworkReply* reply, const Request& initialRequest) const;
|
|
void addReceivedRequest(const Request& request);
|
|
void addHandledRequest(const Request& request);
|
|
void addMatchedRequest(const Request& request, const Rule::Ptr& matchedRule);
|
|
void addUnmatchedRequest(const Request& request);
|
|
void addPassedThroughRequest(const Request& request);
|
|
|
|
bool m_inspectBody;
|
|
BehaviorFlags m_behaviorFlags;
|
|
QPointer<QNetworkAccessManager> m_passThroughNam;
|
|
QVector<Rule::Ptr> m_rules;
|
|
QVector<Request> m_receivedRequests;
|
|
QVector<Request> m_handledRequests;
|
|
QVector<Request> m_matchedRequests;
|
|
QVector<Request> m_unmatchedRequests;
|
|
QVector<Request> m_passedThroughRequests;
|
|
mutable std::unique_ptr<SignalEmitter> m_signalEmitter;
|
|
UnmatchedRequestBehavior m_unmatchedRequestBehavior;
|
|
MockReplyBuilder m_unmatchedRequestBuilder;
|
|
std::unique_ptr<detail::ReplyHeaderHandler> m_replyHeaderHandler;
|
|
QHash<QString, QAuthenticator> m_authenticationCache;
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
|
|
std::unique_ptr<QHash<QString, QHstsPolicy>> m_hstsHash;
|
|
#endif // Qt >= 5.9.0
|
|
};
|
|
|
|
/*! \internal Implementation details.
|
|
*/
|
|
namespace detail
|
|
{
|
|
|
|
inline const char* followedRedirectsPropertyName()
|
|
{
|
|
return "MockNetworkAccess::FollowedRedirects";
|
|
}
|
|
|
|
} // namespace detail
|
|
|
|
// ####### Implementation #######
|
|
|
|
#if defined(MOCKNETWORKACCESSMANAGER_QT_HAS_TEXTCODEC)
|
|
inline StringDecoder::StringDecoder(QTextCodec* codec)
|
|
: m_impl(new TextCodecImpl(codec))
|
|
{
|
|
}
|
|
#endif
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
inline StringDecoder::StringDecoder(std::unique_ptr<QStringDecoder>&& decoder)
|
|
: m_impl{new StringDecoderImpl(std::move(decoder))}
|
|
{
|
|
}
|
|
#endif
|
|
|
|
namespace detail
|
|
{
|
|
|
|
inline bool requestLoadsCookies(const QNetworkRequest& request)
|
|
{
|
|
const QVariant defaultValue = QVariant::fromValue(static_cast<int>(QNetworkRequest::Automatic));
|
|
const int requestValue =
|
|
request.attribute(QNetworkRequest::CookieLoadControlAttribute, defaultValue).toInt();
|
|
return static_cast<QNetworkRequest::LoadControl>(requestValue) == QNetworkRequest::Automatic;
|
|
}
|
|
|
|
} // namespace detail
|
|
|
|
template <class Matcher> Rule& Rule::isMatching(const Matcher& matcher)
|
|
{
|
|
m_predicates.append(Predicates::createGeneric(matcher));
|
|
return *this;
|
|
}
|
|
|
|
template <class Matcher> Rule& Rule::isNotMatching(const Matcher& matcher)
|
|
{
|
|
Predicate::Ptr predicate = Predicates::createGeneric(matcher);
|
|
predicate->negate();
|
|
m_predicates.append(predicate);
|
|
return *this;
|
|
}
|
|
|
|
template <class Base>
|
|
QNetworkReply* Manager<Base>::createRequest(QNetworkAccessManager::Operation operation,
|
|
const QNetworkRequest& origRequest,
|
|
QIODevice* body)
|
|
{
|
|
QByteArray data;
|
|
if (m_inspectBody && body)
|
|
data = body->readAll();
|
|
const QNetworkRequest preparedRequest = prepareRequest(origRequest);
|
|
const Request request(operation, preparedRequest, data);
|
|
|
|
addReceivedRequest(request);
|
|
|
|
QNetworkReply* reply = handleRequest(request);
|
|
finishReply(reply, request);
|
|
return reply;
|
|
}
|
|
|
|
template <class Base> QNetworkRequest Manager<Base>::prepareRequest(const QNetworkRequest& origRequest)
|
|
{
|
|
QNetworkRequest request(origRequest);
|
|
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
|
|
if (this->isStrictTransportSecurityEnabled() && elevateHstsUrl(request.url())) {
|
|
QUrl url = request.url();
|
|
url.setScheme(HttpUtils::httpsScheme());
|
|
if (url.port() == HttpUtils::HttpDefaultPort)
|
|
url.setPort(HttpUtils::HttpsDefaultPort);
|
|
request.setUrl(url);
|
|
}
|
|
#endif // Qt >= 5.9.0
|
|
|
|
const bool loadCookies = detail::requestLoadsCookies(request);
|
|
if (loadCookies) {
|
|
QNetworkCookieJar* cookieJar = this->cookieJar();
|
|
if (cookieJar) {
|
|
QUrl requestUrl = request.url();
|
|
if (requestUrl.path().isEmpty())
|
|
requestUrl.setPath(QStringLiteral("/"));
|
|
QList<QNetworkCookie> cookies = cookieJar->cookiesForUrl(requestUrl);
|
|
if (!cookies.isEmpty())
|
|
request.setHeader(QNetworkRequest::CookieHeader, QVariant::fromValue(cookies));
|
|
}
|
|
}
|
|
|
|
return request;
|
|
}
|
|
|
|
template <class Base> void Manager<Base>::addReceivedRequest(const Request& request)
|
|
{
|
|
m_receivedRequests.append(request);
|
|
if (m_signalEmitter)
|
|
Q_EMIT m_signalEmitter->receivedRequest(request);
|
|
}
|
|
|
|
template <class Base> QNetworkReply* Manager<Base>::handleRequest(const Request& request)
|
|
{
|
|
addHandledRequest(request);
|
|
|
|
if (detail::isDataUrlRequest(request))
|
|
return createDataUrlReply(request);
|
|
|
|
std::unique_ptr<MockReply> mockedReply;
|
|
QVector<Rule::Ptr>::iterator ruleIter = m_rules.begin();
|
|
const QVector<Rule::Ptr>::iterator rulesEnd = m_rules.end();
|
|
for (; ruleIter != rulesEnd; ++ruleIter) {
|
|
Rule::Ptr rule = *ruleIter;
|
|
if (rule->matches(request)) {
|
|
if (rule->passThroughBehavior() != Rule::PassThroughReturnDelegatedReply) {
|
|
mockedReply.reset(rule->createReply(request));
|
|
if (!mockedReply)
|
|
continue;
|
|
}
|
|
|
|
addMatchedRequest(request, rule);
|
|
|
|
if (rule->passThroughBehavior() != Rule::DontPassThrough) {
|
|
std::unique_ptr<QNetworkReply> passThroughReply(passThrough(request, rule->passThroughManager()));
|
|
switch (rule->passThroughBehavior()) {
|
|
case Rule::PassThroughReturnMockReply:
|
|
QObject::connect(
|
|
passThroughReply.get(), SIGNAL(finished()), passThroughReply.get(), SLOT(deleteLater()));
|
|
passThroughReply.release()->setParent(this);
|
|
break;
|
|
case Rule::PassThroughReturnDelegatedReply:
|
|
return passThroughReply.release();
|
|
// LCOV_EXCL_START
|
|
default:
|
|
Q_ASSERT_X(false,
|
|
Q_FUNC_INFO,
|
|
"MockNetworkAccessManager: Internal error: "
|
|
"Unknown Rule::PassThroughBehavior");
|
|
break;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
}
|
|
|
|
prepareReply(mockedReply.get(), request);
|
|
|
|
if (mockedReply->requiresAuthentication()) {
|
|
// POTENTIAL RECURSION
|
|
std::unique_ptr<QNetworkReply> authedReply(authenticateRequest(mockedReply.get(), request));
|
|
if (authedReply) // Did we start a new, authenticated request?
|
|
return authedReply.release();
|
|
}
|
|
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
|
|
if (mockedReply->isRedirectToBeFollowed()) {
|
|
// POTENTIAL RECURSION
|
|
std::unique_ptr<QNetworkReply> redirectedReply(followRedirect(mockedReply.get(), request));
|
|
if (redirectedReply) // Did we actually redirect?
|
|
return redirectedReply.release();
|
|
}
|
|
#endif // Qt >= 5.6.0
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (mockedReply) {
|
|
return mockedReply.release();
|
|
} else {
|
|
addUnmatchedRequest(request);
|
|
switch (m_unmatchedRequestBehavior) {
|
|
case PredefinedReply:
|
|
return m_unmatchedRequestBuilder.createReply();
|
|
case PassThrough:
|
|
return passThrough(request);
|
|
// LCOV_EXCL_START
|
|
default:
|
|
Q_ASSERT_X(false,
|
|
Q_FUNC_INFO,
|
|
QStringLiteral("MockNetworkAccessManager: Unknown behavior for unmatched request: %1")
|
|
.arg(static_cast<int>(m_unmatchedRequestBehavior))
|
|
.toLatin1()
|
|
.constData());
|
|
return Q_NULLPTR;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
}
|
|
}
|
|
|
|
template <class Base> void Manager<Base>::addHandledRequest(const Request& request)
|
|
{
|
|
m_handledRequests.append(request);
|
|
if (m_signalEmitter)
|
|
Q_EMIT m_signalEmitter->handledRequest(request);
|
|
}
|
|
|
|
template <class Base> void Manager<Base>::addMatchedRequest(const Request& request, const Rule::Ptr& matchedRule)
|
|
{
|
|
m_matchedRequests.append(request);
|
|
matchedRule->m_matchedRequests.append(request);
|
|
if (m_signalEmitter)
|
|
Q_EMIT m_signalEmitter->matchedRequest(request, matchedRule);
|
|
}
|
|
|
|
template <class Base> void Manager<Base>::addUnmatchedRequest(const Request& request)
|
|
{
|
|
m_unmatchedRequests.append(request);
|
|
if (m_signalEmitter)
|
|
Q_EMIT m_signalEmitter->unmatchedRequest(request);
|
|
}
|
|
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
|
|
|
|
template <class Base> void Manager<Base>::prepareHstsHash()
|
|
{
|
|
if (!m_hstsHash) {
|
|
m_hstsHash.reset(new QHash<QString, QHstsPolicy>());
|
|
QVector<QHstsPolicy> hstsPolicies = this->strictTransportSecurityHosts();
|
|
|
|
QVector<QHstsPolicy>::const_iterator policyIter = hstsPolicies.cbegin();
|
|
const QVector<QHstsPolicy>::const_iterator policyEnd = hstsPolicies.cend();
|
|
for (; policyIter != policyEnd; ++policyIter) {
|
|
if (!policyIter->isExpired())
|
|
m_hstsHash->insert(policyIter->host(), *policyIter);
|
|
}
|
|
}
|
|
}
|
|
|
|
template <class Base> bool Manager<Base>::elevateHstsUrl(const QUrl& url)
|
|
{
|
|
if (!url.isValid() || url.scheme().toLower() != HttpUtils::httpScheme())
|
|
return false;
|
|
|
|
QString host = url.host();
|
|
const QRegularExpression ipAddressRegEx(QStringLiteral(
|
|
"^\\[.*\\]$|^((25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)$"));
|
|
if (ipAddressRegEx.match(host).hasMatch())
|
|
return false; // Don't elevate IP address URLs
|
|
|
|
prepareHstsHash();
|
|
|
|
// Check if there is a policy for the full host name
|
|
QHash<QString, QHstsPolicy>::Iterator hstsHashIter = m_hstsHash->find(host);
|
|
|
|
if (hstsHashIter != m_hstsHash->end()) {
|
|
if (hstsHashIter.value().isExpired())
|
|
hstsHashIter = m_hstsHash->erase(hstsHashIter);
|
|
else
|
|
return true;
|
|
}
|
|
|
|
// Check if there is a policy for a parent domain
|
|
QStringList domainParts = host.split(QChar::fromLatin1('.'), Qt::SplitBehaviorFlags::SkipEmptyParts);
|
|
domainParts.pop_front();
|
|
|
|
while (!domainParts.isEmpty()) {
|
|
hstsHashIter = m_hstsHash->find(domainParts.join(QChar::fromLatin1('.')));
|
|
if (hstsHashIter != m_hstsHash->end()) {
|
|
if (hstsHashIter.value().isExpired())
|
|
hstsHashIter = m_hstsHash->erase(hstsHashIter);
|
|
else if (hstsHashIter.value().includesSubDomains())
|
|
return true;
|
|
// else we continue because there could be a policy for a another parent domain that includes sub
|
|
// domains
|
|
}
|
|
domainParts.pop_front();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#endif // Qt >= 5.9.0
|
|
|
|
template <class Base> QNetworkReply* Manager<Base>::createDataUrlReply(const Request& request)
|
|
{
|
|
std::unique_ptr<QIODevice> ioDevice(createIODevice(request.body));
|
|
return Base::createRequest(request.operation, request.qRequest, ioDevice.get());
|
|
}
|
|
|
|
template <class Base> void Manager<Base>::prepareReply(MockReply* reply, const Request& request) const
|
|
{
|
|
reply->setBehaviorFlags(m_behaviorFlags);
|
|
reply->prepare(request);
|
|
}
|
|
|
|
template <class Base> void Manager<Base>::finishReply(QNetworkReply* reply, const Request& initialRequest) const
|
|
{
|
|
// Do we want to read out the headers synchronously for mocked replies?
|
|
MockReply* mockedReply = ::qobject_cast<MockReply*>(reply);
|
|
if (mockedReply)
|
|
m_replyHeaderHandler->handleReplyHeaders(reply);
|
|
else
|
|
QObject::connect(reply, SIGNAL(metaDataChanged()), m_replyHeaderHandler.get(), SLOT(handleReplyHeaders()));
|
|
|
|
const RequestList followedRedirects =
|
|
reply->property(detail::followedRedirectsPropertyName()).value<RequestList>();
|
|
/* In case of a real QNetworkReply, we simulate the mocked redirects on the real reply.
|
|
* This would not work with file: or data: URLs since their real signals would have already been emitted.
|
|
* But automatic redirection works only for http: and https: anyway so this is not a problem.
|
|
*/
|
|
RequestList::const_iterator redirectIter = followedRedirects.cbegin();
|
|
const RequestList::const_iterator redirectEnd = followedRedirects.cend();
|
|
for (; redirectIter != redirectEnd; ++redirectIter) {
|
|
const qint64 bodySize = redirectIter->body.size();
|
|
if (bodySize > 0)
|
|
QMetaObject::invokeMethod(
|
|
reply, "uploadProgress", Qt::QueuedConnection, Q_ARG(qint64, bodySize), Q_ARG(qint64, bodySize));
|
|
QMetaObject::invokeMethod(reply, "metaDataChanged", Qt::QueuedConnection);
|
|
QMetaObject::invokeMethod(
|
|
reply, "redirected", Qt::QueuedConnection, Q_ARG(QUrl, redirectIter->qRequest.url()));
|
|
}
|
|
reply->setProperty(detail::followedRedirectsPropertyName(), QVariant());
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
|
if (this->autoDeleteReplies()
|
|
|| initialRequest.qRequest.attribute(QNetworkRequest::AutoDeleteReplyOnFinishAttribute).toBool()) {
|
|
QObject::connect(reply, SIGNAL(finished()), reply, SLOT(deleteLater()));
|
|
}
|
|
#endif // Qt >= 5.14.0
|
|
|
|
if (mockedReply) {
|
|
if (!followedRedirects.isEmpty())
|
|
mockedReply->finish(followedRedirects.last());
|
|
else
|
|
mockedReply->finish(initialRequest);
|
|
}
|
|
}
|
|
|
|
template <class Base> QIODevice* Manager<Base>::createIODevice(const QByteArray& data) const
|
|
{
|
|
QBuffer* buffer = Q_NULLPTR;
|
|
if (m_inspectBody && !data.isNull()) {
|
|
buffer = new QBuffer();
|
|
buffer->setData(data);
|
|
buffer->open(QIODevice::ReadOnly);
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
template <class Base>
|
|
QNetworkReply* Manager<Base>::passThrough(const Request& request, QNetworkAccessManager* overridePassThroughNam)
|
|
{
|
|
std::unique_ptr<QIODevice> ioDevice(createIODevice(request.body));
|
|
|
|
QNetworkAccessManager* passThroughNam =
|
|
overridePassThroughNam ? overridePassThroughNam : static_cast<QNetworkAccessManager*>(m_passThroughNam);
|
|
QNetworkReply* reply;
|
|
if (passThroughNam) {
|
|
switch (request.operation) {
|
|
case QNetworkAccessManager::GetOperation:
|
|
reply = passThroughNam->get(request.qRequest);
|
|
break;
|
|
case QNetworkAccessManager::PostOperation:
|
|
reply = passThroughNam->post(request.qRequest, ioDevice.get());
|
|
break;
|
|
case QNetworkAccessManager::PutOperation:
|
|
reply = passThroughNam->put(request.qRequest, ioDevice.get());
|
|
break;
|
|
case QNetworkAccessManager::HeadOperation:
|
|
reply = passThroughNam->head(request.qRequest);
|
|
break;
|
|
case QNetworkAccessManager::DeleteOperation:
|
|
reply = passThroughNam->deleteResource(request.qRequest);
|
|
break;
|
|
case QNetworkAccessManager::CustomOperation:
|
|
default:
|
|
reply = passThroughNam->sendCustomRequest(
|
|
request.qRequest,
|
|
request.qRequest.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray(),
|
|
ioDevice.get());
|
|
break;
|
|
}
|
|
} else
|
|
reply = Base::createRequest(request.operation, request.qRequest, ioDevice.get());
|
|
if (ioDevice) {
|
|
QObject::connect(reply, SIGNAL(finished()), ioDevice.get(), SLOT(deleteLater()));
|
|
ioDevice.release()->setParent(reply);
|
|
}
|
|
QObject::connect(reply, SIGNAL(metaDataChanged()), m_replyHeaderHandler.get(), SLOT(handleReplyHeaders()));
|
|
addPassedThroughRequest(request);
|
|
return reply;
|
|
}
|
|
|
|
template <class Base> void Manager<Base>::addPassedThroughRequest(const Request& request)
|
|
{
|
|
m_passedThroughRequests.append(request);
|
|
if (m_signalEmitter)
|
|
Q_EMIT m_signalEmitter->passedThrough(request);
|
|
}
|
|
|
|
template <class Base>
|
|
QNetworkReply* Manager<Base>::authenticateRequest(MockReply* unauthedReply, const Request& unauthedReq)
|
|
{
|
|
typedef QVector<HttpUtils::Authentication::Challenge::Ptr> ChallengeVector;
|
|
ChallengeVector authChallenges = HttpUtils::Authentication::getAuthenticationChallenges(unauthedReply);
|
|
|
|
if (authChallenges.isEmpty()) {
|
|
qCWarning(log) << "Missing authentication challenge in reply"
|
|
<< detail::pointerToQString(unauthedReply).toLatin1().data();
|
|
return Q_NULLPTR;
|
|
}
|
|
|
|
/* Select the strongest challenge.
|
|
* If there are multiple challenges with the same strength,
|
|
* the last one is used according to the order they appear in the HTTP headers.
|
|
*/
|
|
std::stable_sort(
|
|
authChallenges.begin(), authChallenges.end(), HttpUtils::Authentication::Challenge::StrengthCompare());
|
|
HttpUtils::Authentication::Challenge::Ptr authChallenge = authChallenges.last();
|
|
|
|
QAuthenticator authenticator = getAuthenticator(unauthedReply, unauthedReq, authChallenge);
|
|
if (authenticator.user().isNull() && authenticator.password().isNull())
|
|
return Q_NULLPTR;
|
|
|
|
QNetworkRequest authedQReq(unauthedReq.qRequest);
|
|
authChallenge->addAuthorization(authedQReq, unauthedReq.operation, unauthedReq.body, authenticator);
|
|
const Request authedReq(unauthedReq.operation, authedQReq, unauthedReq.body);
|
|
QNetworkReply* authedReply = this->handleRequest(authedReq); // POTENTIAL RECURSION
|
|
return authedReply;
|
|
}
|
|
|
|
template <class Base>
|
|
QAuthenticator Manager<Base>::getAuthenticator(MockReply* unauthedReply,
|
|
const Request& unauthedReq,
|
|
const HttpUtils::Authentication::Challenge::Ptr& authChallenge)
|
|
{
|
|
const QString realm = authChallenge->realm().toLower(); // realm is case-insensitive
|
|
const QUrl authScope = HttpUtils::Authentication::authenticationScopeForUrl(unauthedReply->url());
|
|
const QString authKey = realm + QChar::fromLatin1('\x1C') + authScope.toString(QUrl::FullyEncoded);
|
|
const QNetworkRequest::LoadControl authReuse = static_cast<QNetworkRequest::LoadControl>(
|
|
unauthedReq.qRequest
|
|
.attribute(QNetworkRequest::AuthenticationReuseAttribute, static_cast<int>(QNetworkRequest::Automatic))
|
|
.toInt());
|
|
|
|
if (authReuse == QNetworkRequest::Automatic && m_authenticationCache.contains(authKey))
|
|
return m_authenticationCache.value(authKey);
|
|
else {
|
|
QAuthenticator authenticator;
|
|
authenticator.setOption(HttpUtils::Authentication::Basic::realmKey(), realm);
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)
|
|
authenticator.setRealm(realm);
|
|
#endif // Qt >= 5.4.0
|
|
Q_EMIT this->authenticationRequired(unauthedReply, &authenticator);
|
|
if (!authenticator.user().isNull() || !authenticator.password().isNull())
|
|
m_authenticationCache.insert(authKey, authenticator);
|
|
return authenticator;
|
|
}
|
|
}
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
|
|
|
|
/*! \internal Implementation details
|
|
*/
|
|
namespace detail
|
|
{
|
|
/*! Checks if a redirect would cause a security degradation.
|
|
* \param from The URL from which the request is redirected.
|
|
* \param to The target URL of the redirect.
|
|
* \return \c true if a redirect from \p from to \p to degrades protocol security (for example, HTTPS to HTTP).
|
|
*/
|
|
inline bool secureToUnsecureRedirect(const QUrl& from, const QUrl& to)
|
|
{
|
|
return from.scheme().toLower() == HttpUtils::httpsScheme()
|
|
&& to.scheme().toLower() == HttpUtils::httpScheme();
|
|
}
|
|
|
|
/*! Checks if two URLs refer to the same origin.
|
|
*
|
|
* \param left One QUrl to compare.
|
|
* \param right The other QUrl to compare.
|
|
* \return \c true if \p left and \p right refer to the same origin.
|
|
*/
|
|
inline bool isSameOrigin(const QUrl& left, const QUrl& right)
|
|
{
|
|
return left.scheme() == right.scheme() && left.host() == right.host() && left.port() == right.port();
|
|
}
|
|
} // namespace detail
|
|
|
|
template <class Base> QNetworkReply* Manager<Base>::followRedirect(MockReply* prevReply, const Request& prevReq)
|
|
{
|
|
using namespace detail;
|
|
|
|
const QUrl prevTarget = prevReq.qRequest.url();
|
|
const QUrl nextTarget = prevTarget.resolved(prevReply->locationHeader());
|
|
const QString nextTargetScheme = nextTarget.scheme().toLower();
|
|
const QVariant statusCodeAttr = prevReply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
|
|
|
if (!nextTarget.isValid()
|
|
|| (nextTargetScheme != HttpUtils::httpScheme() && nextTargetScheme != HttpUtils::httpsScheme())) {
|
|
prevReply->setError(QNetworkReply::ProtocolUnknownError);
|
|
prevReply->setAttribute(QNetworkRequest::RedirectionTargetAttribute, QVariant());
|
|
return Q_NULLPTR;
|
|
}
|
|
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
|
|
const QVariant redirectPolicyAttr = prevReq.qRequest.attribute(QNetworkRequest::RedirectPolicyAttribute);
|
|
if (redirectPolicyAttr.isValid()) {
|
|
QNetworkRequest::RedirectPolicy redirectPolicy =
|
|
static_cast<QNetworkRequest::RedirectPolicy>(redirectPolicyAttr.toInt());
|
|
if (!applyRedirectPolicy(redirectPolicy, prevReply, prevReq.qRequest, nextTarget))
|
|
return Q_NULLPTR;
|
|
} else
|
|
#endif // Qt >= 5.9.0
|
|
{
|
|
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && QT_DEPRECATED_SINCE(5, 15))
|
|
QVariant followRedirectsAttr = prevReq.qRequest.attribute(QNetworkRequest::FollowRedirectsAttribute);
|
|
if (followRedirectsAttr.isValid()) {
|
|
if (!followRedirectsAttr.toBool())
|
|
return Q_NULLPTR;
|
|
|
|
if (detail::secureToUnsecureRedirect(prevTarget, nextTarget)) {
|
|
prevReply->setError(QNetworkReply::InsecureRedirectError);
|
|
return Q_NULLPTR;
|
|
}
|
|
} else
|
|
#endif // Qt < 6.0.0
|
|
{
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
|
|
if (!applyRedirectPolicy(this->redirectPolicy(), prevReply, prevReq.qRequest, nextTarget))
|
|
return Q_NULLPTR;
|
|
#else // Qt < 5.9.0
|
|
// Following the redirect is not requested
|
|
return Q_NULLPTR;
|
|
#endif // Qt >= 5.9.0
|
|
}
|
|
}
|
|
|
|
if (prevReq.qRequest.maximumRedirectsAllowed() <= 0) {
|
|
prevReply->setError(QNetworkReply::TooManyRedirectsError);
|
|
return Q_NULLPTR;
|
|
}
|
|
|
|
QNetworkAccessManager::Operation nextOperation;
|
|
QByteArray nextReqBody;
|
|
if (prevReq.operation == QNetworkAccessManager::GetOperation
|
|
|| prevReq.operation == QNetworkAccessManager::HeadOperation)
|
|
nextOperation = prevReq.operation;
|
|
else if (m_behaviorFlags.testFlag(Behavior_RedirectWithGet))
|
|
// Qt up to 5.9.3 always redirects with a GET
|
|
nextOperation = QNetworkAccessManager::GetOperation;
|
|
else {
|
|
nextOperation = prevReq.operation;
|
|
nextReqBody = prevReq.body;
|
|
|
|
switch (static_cast<HttpStatus::Code>(statusCodeAttr.toInt())) {
|
|
case HttpStatus::TemporaryRedirect: // 307
|
|
case HttpStatus::PermanentRedirect: // 308
|
|
break;
|
|
case HttpStatus::MovedPermanently: // 301
|
|
case HttpStatus::Found: // 302
|
|
if (!m_behaviorFlags.testFlag(Behavior_IgnoreSafeRedirectMethods)
|
|
&& usesSafeRedirectRequestMethod(prevReq)) {
|
|
break;
|
|
}
|
|
// Fall through
|
|
case HttpStatus::SeeOther: // 303
|
|
default:
|
|
nextOperation = QNetworkAccessManager::GetOperation;
|
|
nextReqBody.clear();
|
|
break;
|
|
}
|
|
}
|
|
|
|
QNetworkRequest nextQReq(prevReq.qRequest);
|
|
nextQReq.setUrl(nextTarget);
|
|
nextQReq.setMaximumRedirectsAllowed(prevReq.qRequest.maximumRedirectsAllowed() - 1);
|
|
if (nextOperation != QNetworkAccessManager::CustomOperation)
|
|
nextQReq.setAttribute(QNetworkRequest::CustomVerbAttribute, QVariant());
|
|
|
|
Request nextReq(nextOperation, nextQReq, nextReqBody);
|
|
QNetworkReply* redirectReply = this->handleRequest(nextReq); // POTENTIAL RECURSION
|
|
|
|
RequestList followedRedirects = redirectReply->property(followedRedirectsPropertyName()).value<RequestList>();
|
|
followedRedirects.prepend(nextReq);
|
|
redirectReply->setProperty(followedRedirectsPropertyName(), QVariant::fromValue(followedRedirects));
|
|
|
|
MockReply* mockedReply = ::qobject_cast<MockReply*>(redirectReply);
|
|
if (mockedReply)
|
|
mockedReply->setUrl(nextQReq.url());
|
|
|
|
return redirectReply;
|
|
}
|
|
|
|
#endif // Qt >= 5.6.0
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
|
|
|
|
template <class Base>
|
|
bool Manager<Base>::applyRedirectPolicy(QNetworkRequest::RedirectPolicy policy,
|
|
MockReply* prevReply,
|
|
const QNetworkRequest& prevRequest,
|
|
const QUrl& redirectTarget)
|
|
{
|
|
const QUrl prevTarget = prevRequest.url();
|
|
switch (policy) {
|
|
case QNetworkRequest::ManualRedirectPolicy:
|
|
return false;
|
|
case QNetworkRequest::NoLessSafeRedirectPolicy:
|
|
if (detail::secureToUnsecureRedirect(prevTarget, redirectTarget)) {
|
|
prevReply->setError(QNetworkReply::InsecureRedirectError);
|
|
return false;
|
|
}
|
|
break;
|
|
case QNetworkRequest::SameOriginRedirectPolicy:
|
|
if (!detail::isSameOrigin(prevTarget, redirectTarget)) {
|
|
prevReply->setError(QNetworkReply::InsecureRedirectError);
|
|
return false;
|
|
}
|
|
break;
|
|
case QNetworkRequest::UserVerifiedRedirectPolicy:
|
|
// TODO: QNetworkRequest::UserVerifiedRedirectPolicy
|
|
/* Does that even make sense? We would probably need to make the limitation that the
|
|
* QNetworkReply::redirectAllowed() signal must be emitted synchronously inside the slot handling
|
|
* the QNetworkReply::redirected() signal.
|
|
* Or we would need to return a proxy QNetworkReply from the Manager which is then "filled" and "finished"
|
|
* with either a MockReply or a real QNetworkReply after the redirection(s).
|
|
*/
|
|
qCWarning(log) << "User verified redirection policy is not supported at the moment";
|
|
prevReply->setError(QNetworkReply::InsecureRedirectError);
|
|
return false;
|
|
break;
|
|
// LCOV_EXCL_START
|
|
default:
|
|
qCWarning(log) << "Unknown redirect policy:" << policy;
|
|
prevReply->setError(QNetworkReply::InsecureRedirectError);
|
|
return false;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#endif // Qt >= 5.9.0
|
|
|
|
} // namespace MockNetworkAccess
|
|
|
|
Q_DECLARE_METATYPE(MockNetworkAccess::MockReplyBuilder)
|
|
Q_DECLARE_METATYPE(MockNetworkAccess::HttpStatus::Code)
|
|
Q_DECLARE_OPERATORS_FOR_FLAGS(MockNetworkAccess::BehaviorFlags)
|
|
Q_DECLARE_METATYPE(MockNetworkAccess::RequestList)
|
|
|
|
#endif /* MOCKNETWORKACCESSMANAGER_HPP */
|