Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package qt6-websockets for openSUSE:Factory checked in at 2023-10-13 23:14:46 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/qt6-websockets (Old) and /work/SRC/openSUSE:Factory/.qt6-websockets.new.20540 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "qt6-websockets" Fri Oct 13 23:14:46 2023 rev:17 rq:1116964 version:6.6.0 Changes: -------- --- /work/SRC/openSUSE:Factory/qt6-websockets/qt6-websockets.changes 2023-10-02 20:08:29.527175457 +0200 +++ /work/SRC/openSUSE:Factory/.qt6-websockets.new.20540/qt6-websockets.changes 2023-10-13 23:15:49.761535999 +0200 @@ -1,0 +2,6 @@ +Tue Oct 10 09:40:04 UTC 2023 - Christophe Marin <christo...@krop.fr> + +- Update to 6.6.0 + * https://www.qt.io/blog/qt-6.6-released + +------------------------------------------------------------------- Old: ---- qtwebsockets-everywhere-src-6.5.3.tar.xz New: ---- qtwebsockets-everywhere-src-6.6.0.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ qt6-websockets.spec ++++++ --- /var/tmp/diff_new_pack.BVN5x0/_old 2023-10-13 23:15:50.557564871 +0200 +++ /var/tmp/diff_new_pack.BVN5x0/_new 2023-10-13 23:15:50.561565016 +0200 @@ -16,8 +16,8 @@ # -%define real_version 6.5.3 -%define short_version 6.5 +%define real_version 6.6.0 +%define short_version 6.6 %define tar_name qtwebsockets-everywhere-src %define tar_suffix %{nil} # @@ -27,7 +27,7 @@ %endif # Name: qt6-websockets%{?pkg_suffix} -Version: 6.5.3 +Version: 6.6.0 Release: 0 Summary: Qt 6 WebSockets library License: LGPL-3.0-only OR (GPL-2.0-only OR GPL-3.0-or-later) ++++++ qtwebsockets-everywhere-src-6.5.3.tar.xz -> qtwebsockets-everywhere-src-6.6.0.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtwebsockets-everywhere-src-6.5.3/.cmake.conf new/qtwebsockets-everywhere-src-6.6.0/.cmake.conf --- old/qtwebsockets-everywhere-src-6.5.3/.cmake.conf 2023-09-24 11:45:46.000000000 +0200 +++ new/qtwebsockets-everywhere-src-6.6.0/.cmake.conf 2023-10-03 20:26:29.000000000 +0200 @@ -1,3 +1,3 @@ -set(QT_REPO_MODULE_VERSION "6.5.3") +set(QT_REPO_MODULE_VERSION "6.6.0") set(QT_REPO_MODULE_PRERELEASE_VERSION_SEGMENT "alpha1") set(QT_EXTRA_INTERNAL_TARGET_DEFINES "QT_NO_AS_CONST=1") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtwebsockets-everywhere-src-6.5.3/.tag new/qtwebsockets-everywhere-src-6.6.0/.tag --- old/qtwebsockets-everywhere-src-6.5.3/.tag 2023-09-24 11:45:46.000000000 +0200 +++ new/qtwebsockets-everywhere-src-6.6.0/.tag 2023-10-03 20:26:29.000000000 +0200 @@ -1 +1 @@ -625524eb7e7518a61c9cfa2ba2eb1cd2673d5cf3 +090fb14fa3011d2590ac28dfc37f2c6e2afe09b1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtwebsockets-everywhere-src-6.5.3/coin/axivion/ci_config_linux.json new/qtwebsockets-everywhere-src-6.6.0/coin/axivion/ci_config_linux.json --- old/qtwebsockets-everywhere-src-6.5.3/coin/axivion/ci_config_linux.json 1970-01-01 01:00:00.000000000 +0100 +++ new/qtwebsockets-everywhere-src-6.6.0/coin/axivion/ci_config_linux.json 2023-10-03 20:26:29.000000000 +0200 @@ -0,0 +1,60 @@ +{ + "Project": { + "Git": { + "_active": true, + "sourceserver_gitdir": "/data/axivion/databases/$(env:TESTED_MODULE_COIN).git" + }, + "BuildSystemIntegration": { + "child_order": [ + "GCCSetup", + "CMake", + "LinkLibraries" + ] + }, + "CMake": { + "_active": true, + "_copy_from": "CMakeIntegration", + "build_environment": {}, + "build_options": "-j4", + "generate_options": "--fresh", + "generator": "Ninja" + }, + "GCCSetup": { + "_active": true, + "_copy_from": "Command", + "build_command": "gccsetup --cc gcc --cxx g++ --config ../../../axivion/" + }, + "LinkLibraries": { + "_active": true, + "_copy_from": "AxivionLinker", + "input_files": [ + "build/lib/lib*.so*.ir", + "build/qml/*/lib*.so*.ir" + ], + "ir": "build/$(env:TESTED_MODULE_COIN).ir" + }, + "Project-GlobalOptions": { + "directory": "../work/qt/$(env:TESTED_MODULE_COIN)", + "ir": "build/$(env:TESTED_MODULE_COIN).ir", + "name": "qt_$(env:TESTED_MODULE_COIN)_dev_$(env:TARGET_OS_COIN)" + } + }, + "Results": { + "Dashboard": { + "dashboard_url": "https://axivion-srv.ci.qt.io/axivion/" + }, + "Database": { + "ci_mode": { + "directory": "/data/axivion/databases" + } + } + }, + "_Format": "1.0", + "_Version": "trunk-9e0ef9c5818", + "_VersionNum": [ + 7, + 6, + 9999, + 11489 + ] +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtwebsockets-everywhere-src-6.5.3/dependencies.yaml new/qtwebsockets-everywhere-src-6.6.0/dependencies.yaml --- old/qtwebsockets-everywhere-src-6.5.3/dependencies.yaml 2023-09-24 11:45:46.000000000 +0200 +++ new/qtwebsockets-everywhere-src-6.6.0/dependencies.yaml 2023-10-03 20:26:29.000000000 +0200 @@ -1,7 +1,7 @@ dependencies: ../qtbase: - ref: 372eaedc5b8c771c46acc4c96e91bbade4ca3624 + ref: 33f5e985e480283bb0ca9dea5f82643e825ba87c required: true ../qtdeclarative: - ref: e00c258fa5a4e122636d441967dea035865fac5d + ref: e559d5cf2b66c4a973f83f173d57676a21d287ef required: false diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtwebsockets-everywhere-src-6.5.3/src/websockets/qwebsocket.cpp new/qtwebsockets-everywhere-src-6.6.0/src/websockets/qwebsocket.cpp --- old/qtwebsockets-everywhere-src-6.5.3/src/websockets/qwebsocket.cpp 2023-09-24 11:45:46.000000000 +0200 +++ new/qtwebsockets-everywhere-src-6.6.0/src/websockets/qwebsocket.cpp 2023-10-03 20:26:29.000000000 +0200 @@ -98,6 +98,27 @@ \sa QAuthenticator, QNetworkProxy */ + +/*! + \fn void QWebSocket::authenticationRequired(QAuthenticator *authenticator) + \since 6.6 + + This signal is emitted when the server requires authentication. + The \a authenticator object must then be filled in with the required details + to allow authentication and continue the connection. + + If you know that the server may require authentication, you can set the + username and password on the initial QUrl, using QUrl::setUserName and + QUrl::setPassword. QWebSocket will still try to connect \e{once} without + using the provided credentials. + + \note It is not possible to use a QueuedConnection to connect to + this signal, as the connection will fail if the authenticator has + not been filled in with new information when the signal returns. + + \sa QAuthenticator +*/ + /*! \fn void QWebSocket::stateChanged(QAbstractSocket::SocketState state); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtwebsockets-everywhere-src-6.5.3/src/websockets/qwebsocket.h new/qtwebsockets-everywhere-src-6.6.0/src/websockets/qwebsocket.h --- old/qtwebsockets-everywhere-src-6.5.3/src/websockets/qwebsocket.h 2023-09-24 11:45:46.000000000 +0200 +++ new/qtwebsockets-everywhere-src-6.6.0/src/websockets/qwebsocket.h 2023-10-03 20:26:29.000000000 +0200 @@ -17,8 +17,11 @@ #include "QtWebSockets/qwebsockets_global.h" #include "QtWebSockets/qwebsocketprotocol.h" +#include <QtCore/qobject.h> + QT_BEGIN_NAMESPACE +class QAuthenticator; class QTcpSocket; class QWebSocketPrivate; class QMaskGenerator; @@ -118,6 +121,7 @@ #ifndef QT_NO_NETWORKPROXY void proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *pAuthenticator); #endif + void authenticationRequired(QAuthenticator *authenticator); void readChannelFinished(); void textFrameReceived(const QString &frame, bool isLastFrame); void binaryFrameReceived(const QByteArray &frame, bool isLastFrame); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtwebsockets-everywhere-src-6.5.3/src/websockets/qwebsocket_p.cpp new/qtwebsockets-everywhere-src-6.6.0/src/websockets/qwebsocket_p.cpp --- old/qtwebsockets-everywhere-src-6.5.3/src/websockets/qwebsocket_p.cpp 2023-09-24 11:45:46.000000000 +0200 +++ new/qtwebsockets-everywhere-src-6.6.0/src/websockets/qwebsocket_p.cpp 2023-10-03 20:26:29.000000000 +0200 @@ -28,13 +28,17 @@ #endif #include <QtNetwork/private/qhttpheaderparser_p.h> +#include <QtNetwork/private/qauthenticator_p.h> #include <QtCore/QDebug> #include <limits> +#include <memory> QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + namespace { constexpr quint64 MAX_OUTGOING_FRAME_SIZE_IN_BYTES = std::numeric_limits<int>::max() - 1; @@ -963,6 +967,17 @@ return written; } +static QString msgUnsupportedAuthenticateChallenges(qsizetype count) +{ + // Keep the error on a single line so it can easily be searched for: + //: 'WWW-Authenticate' is the HTTP header. + return count == 1 + ? QWebSocket::tr("QWebSocketPrivate::processHandshake: " + "Unsupported WWW-Authenticate challenge encountered.") + : QWebSocket::tr("QWebSocketPrivate::processHandshake: " + "Unsupported WWW-Authenticate challenges encountered."); +} + //called on the client for a server handshake response /*! \internal @@ -1040,7 +1055,8 @@ QByteArrayLiteral("sec-websocket-version"))); bool ok = false; QString errorDescription; - if (Q_LIKELY(parser.getStatusCode() == 101)) { + switch (parser.getStatusCode()) { + case 101: { //HTTP/x.y 101 Switching Protocols //TODO: do not check the httpStatusText right now ok = (acceptKey.size() > 0 @@ -1064,7 +1080,9 @@ errorDescription = QWebSocket::tr( "Invalid parameter encountered during protocol upgrade: %1").arg(upgradeParms); } - } else if (parser.getStatusCode() == 400) { + break; + } + case 400: { //HTTP/1.1 400 Bad Request if (!version.isEmpty()) { const QStringList versions = version.split(QStringLiteral(", "), Qt::SkipEmptyParts); @@ -1083,17 +1101,72 @@ errorDescription = QWebSocket::tr("QWebSocketPrivate::processHandshake: Unknown error condition encountered. Aborting connection."); } - } else { + break; + } + case 401: { + // HTTP/1.1 401 UNAUTHORIZED + if (m_authenticator.isNull()) + m_authenticator.detach(); + auto *priv = QAuthenticatorPrivate::getPrivate(m_authenticator); + const QList<QByteArray> challenges = parser.headerFieldValues("WWW-Authenticate"); + const bool isSupported = std::any_of(challenges.begin(), challenges.end(), + QAuthenticatorPrivate::isMethodSupported); + if (isSupported) + priv->parseHttpResponse(parser.headers(), /*isProxy=*/false, m_request.url().host()); + if (!isSupported || priv->method == QAuthenticatorPrivate::None) { + errorDescription = msgUnsupportedAuthenticateChallenges(challenges.size()); + break; + } + + const QUrl url = m_request.url(); + const bool hasCredentials = !url.userName().isEmpty() || !url.password().isEmpty(); + if (hasCredentials) { + m_authenticator.setUser(url.userName()); + m_authenticator.setPassword(url.password()); + // Unset username and password so we don't try it again + QUrl copy = url; + copy.setUserName({}); + copy.setPassword({}); + m_request.setUrl(copy); + } + if (priv->phase == QAuthenticatorPrivate::Done) { // No user/pass from URL: + emit q->authenticationRequired(&m_authenticator); + if (priv->phase == QAuthenticatorPrivate::Done) { + // user/pass was not updated: + errorDescription = QWebSocket::tr( + "QWebSocket::processHandshake: Host requires authentication"); + break; + } + } + m_needsResendWithCredentials = true; + if (parser.firstHeaderField("Connection").compare("close", Qt::CaseInsensitive) == 0) + m_needsReconnect = true; + else + m_bytesToSkipBeforeNewResponse = parser.firstHeaderField("Content-Length").toInt(); + break; + } + default: { errorDescription = QWebSocket::tr("QWebSocketPrivate::processHandshake: Unhandled http status code: %1 (%2).") .arg(parser.getStatusCode()).arg(parser.getReasonPhrase()); } + } if (ok) { // handshake succeeded setProtocol(protocol); setSocketState(QAbstractSocket::ConnectedState); Q_EMIT q->connected(); + } else if (m_needsResendWithCredentials) { + if (m_needsReconnect && m_pSocket->state() != QAbstractSocket::UnconnectedState) { + // Disconnect here, then in processStateChanged() we reconnect when + // we are unconnected. + m_pSocket->disconnectFromHost(); + } else { + // I'm cheating, this is how a handshake starts: + processStateChanged(QAbstractSocket::ConnectedState); + } + return; } else { // handshake failed setErrorString(errorDescription); @@ -1132,6 +1205,27 @@ } const QStringList subProtocols = requestedSubProtocols(); + // Perform authorization if needed: + if (m_needsResendWithCredentials) { + m_needsResendWithCredentials = false; + // Based on QHttpNetworkRequest::uri: + auto uri = [](QUrl url) -> QByteArray { + QUrl::FormattingOptions format(QUrl::RemoveFragment | QUrl::RemoveUserInfo + | QUrl::FullyEncoded); + if (url.path().isEmpty()) + url.setPath(QStringLiteral("/")); + else + format |= QUrl::NormalizePathSegments; + return url.toEncoded(format); + }; + auto *priv = QAuthenticatorPrivate::getPrivate(m_authenticator); + Q_ASSERT(priv); + QByteArray response = priv->calculateResponse("GET", uri(m_request.url()), + m_request.url().host()); + if (!response.isEmpty()) + headers << qMakePair(u"Authorization"_s, QString::fromLatin1(response)); + } + const auto format = QUrl::RemoveScheme | QUrl::RemoveUserInfo | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment; @@ -1158,7 +1252,28 @@ break; case QAbstractSocket::UnconnectedState: - if (webSocketState != QAbstractSocket::UnconnectedState) { + if (m_needsReconnect) { + // Need to reinvoke the lambda queued because the underlying socket + // isn't done cleaning up yet... + auto reconnect = [this]() { + m_needsReconnect = false; + const QUrl url = m_request.url(); +#if QT_CONFIG(ssl) + const bool isEncrypted = url.scheme().compare(u"wss", Qt::CaseInsensitive) == 0; + if (isEncrypted) { + // This has to work because we did it earlier; this is just us + // reconnecting! + auto *sslSocket = qobject_cast<QSslSocket *>(m_pSocket); + Q_ASSERT(sslSocket); + sslSocket->connectToHostEncrypted(url.host(), quint16(url.port(443))); + } else +#endif + { + m_pSocket->connectToHost(url.host(), quint16(url.port(80))); + } + }; + QMetaObject::invokeMethod(q, reconnect, Qt::QueuedConnection); + } else if (webSocketState != QAbstractSocket::UnconnectedState) { setSocketState(QAbstractSocket::UnconnectedState); Q_EMIT q->disconnected(); } @@ -1189,7 +1304,9 @@ if (!m_pSocket) // disconnected with data still in-bound return; if (state() == QAbstractSocket::ConnectingState) { - if (!m_pSocket->canReadLine()) + if (m_bytesToSkipBeforeNewResponse > 0) + m_bytesToSkipBeforeNewResponse -= m_pSocket->skip(m_bytesToSkipBeforeNewResponse); + if (m_bytesToSkipBeforeNewResponse > 0 || !m_pSocket->canReadLine()) return; processHandshake(m_pSocket); // That may have changed state(), recheck in the next 'if' below. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtwebsockets-everywhere-src-6.5.3/src/websockets/qwebsocket_p.h new/qtwebsockets-everywhere-src-6.6.0/src/websockets/qwebsocket_p.h --- old/qtwebsockets-everywhere-src-6.5.3/src/websockets/qwebsocket_p.h 2023-09-24 11:45:46.000000000 +0200 +++ new/qtwebsockets-everywhere-src-6.6.0/src/websockets/qwebsocket_p.h 2023-10-03 20:26:29.000000000 +0200 @@ -20,6 +20,7 @@ #ifndef QT_NO_NETWORKPROXY #include <QtNetwork/QNetworkProxy> #endif +#include <QtNetwork/QAuthenticator> #ifndef QT_NO_SSL #include <QtNetwork/QSslConfiguration> #include <QtNetwork/QSslError> @@ -211,12 +212,21 @@ QAbstractSocket::PauseModes m_pauseMode; qint64 m_readBufferSize; + // For WWW-Authenticate handling + QAuthenticator m_authenticator; + qint64 m_bytesToSkipBeforeNewResponse = 0; + QByteArray m_key; //identification key used in handshake requests bool m_mustMask; //a server must not mask the frames it sends bool m_isClosingHandshakeSent; bool m_isClosingHandshakeReceived; + + // For WWW-Authenticate handling + bool m_needsResendWithCredentials = false; + bool m_needsReconnect = false; + QWebSocketProtocol::CloseCode m_closeCode; QString m_closeReason; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtwebsockets-everywhere-src-6.5.3/tests/auto/websockets/qwebsocket/CMakeLists.txt new/qtwebsockets-everywhere-src-6.6.0/tests/auto/websockets/qwebsocket/CMakeLists.txt --- old/qtwebsockets-everywhere-src-6.5.3/tests/auto/websockets/qwebsocket/CMakeLists.txt 2023-09-24 11:45:46.000000000 +0200 +++ new/qtwebsockets-everywhere-src-6.6.0/tests/auto/websockets/qwebsocket/CMakeLists.txt 2023-10-03 20:26:29.000000000 +0200 @@ -14,5 +14,18 @@ Qt::WebSockets ) +set(qwebsocketshared_resource_files + "../shared/localhost.cert" + "../shared/localhost.key" +) +qt_internal_add_resource(tst_qwebsocket "qwebsocketshared" + PREFIX + "/" + BASE + "../shared" + FILES + ${qwebsocketshared_resource_files} +) + #### Keys ignored in scope 1:.:.:qwebsocket.pro:<TRUE>: # TEMPLATE = "app" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtwebsockets-everywhere-src-6.5.3/tests/auto/websockets/qwebsocket/tst_qwebsocket.cpp new/qtwebsockets-everywhere-src-6.6.0/tests/auto/websockets/qwebsocket/tst_qwebsocket.cpp --- old/qtwebsockets-everywhere-src-6.5.3/tests/auto/websockets/qwebsocket/tst_qwebsocket.cpp 2023-09-24 11:45:46.000000000 +0200 +++ new/qtwebsockets-everywhere-src-6.6.0/tests/auto/websockets/qwebsocket/tst_qwebsocket.cpp 2023-10-03 20:26:29.000000000 +0200 @@ -10,6 +10,16 @@ #include <QtWebSockets/qwebsocketprotocol.h> #include <QtNetwork/qtcpserver.h> +#include <QtNetwork/qauthenticator.h> +#include <QtNetwork/qtcpsocket.h> + +#if QT_CONFIG(ssl) +#include <QtNetwork/qsslserver.h> +#include <QtNetwork/qsslcertificate.h> +#include <QtNetwork/qsslkey.h> +#endif + +#include <utility> QT_USE_NAMESPACE @@ -143,6 +153,8 @@ #ifndef QT_NO_NETWORKPROXY void tst_setProxy(); #endif + void authenticationRequired_data(); + void authenticationRequired(); void overlongCloseReason(); void incomingMessageTooLong(); void incomingFrameTooLong(); @@ -921,6 +933,308 @@ } #endif // QT_NO_NETWORKPROXY +class AuthServer : public QTcpServer +{ + Q_OBJECT +public: + AuthServer() + { + connect(this, &QTcpServer::pendingConnectionAvailable, this, &AuthServer::handleConnection); + } + + void incomingConnection(qintptr sockfd) override + { + if (withEncryption) { +#if QT_CONFIG(ssl) + auto *sslSocket = new QSslSocket(this); + connect(sslSocket, &QSslSocket::encrypted, this, + [this, sslSocket]() { + addPendingConnection(sslSocket); + }); + sslSocket->setSslConfiguration(configuration); + sslSocket->setSocketDescriptor(sockfd); + sslSocket->startServerEncryption(); +#else + QFAIL("withEncryption should not be 'true' if we don't have TLS"); +#endif + } else { + QTcpSocket *socket = new QTcpSocket(this); + socket->setSocketDescriptor(sockfd); + addPendingConnection(socket); + } + } + + void handleConnection() + { + QTcpSocket *serverSocket = nextPendingConnection(); + connect(serverSocket, &QTcpSocket::readyRead, this, &AuthServer::handleReadyRead); + } + + void handleReadyRead() + { + auto *serverSocket = qobject_cast<QTcpSocket *>(sender()); + incomingData.append(serverSocket->readAll()); + if (finished) { + qWarning() << "Unexpected trailing data..." << incomingData; + return; + } + if (!incomingData.contains("\r\n\r\n")) { + qDebug("Not all of the data arrived at once, waiting for more..."); + return; + } + // Move incomingData into local variable and reset it since we received it all: + const QByteArray fullHeader = std::exchange(incomingData, {}); + + QLatin1StringView authView = getHeaderValue("Authorization"_L1, fullHeader); + if (authView.isEmpty()) + return writeAuthRequired(serverSocket); + qsizetype sep = authView.indexOf(' '); + if (sep == -1) + return writeAuthRequired(serverSocket); + QLatin1StringView authenticateMethod = authView.first(sep); + QLatin1StringView authenticateAttempt = authView.sliced(sep + 1); + if (authenticateMethod != "Basic" || authenticateAttempt != expectedBasicPayload()) + return writeAuthRequired(serverSocket); + + QLatin1StringView keyView = getHeaderValue("Sec-WebSocket-Key"_L1, fullHeader); + QVERIFY(!keyView.isEmpty()); + + const QByteArray accept = + QByteArrayView(keyView) % QByteArrayLiteral("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + auto generatedKey = QCryptographicHash::hash(accept, QCryptographicHash::Sha1).toBase64(); + serverSocket->write("HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: " % generatedKey % "\r\n" + "\r\n"); + finished = true; + } + + void writeAuthRequired(QTcpSocket *socket) const + { + QByteArray payload = "HTTP/1.1 401 UNAUTHORIZED\r\n" + "WWW-Authenticate: Basic realm=shadow\r\n"; + if (withConnectionClose) + payload.append("Connection: Close\r\n"); + else if (withContentLength) + payload.append("Content-Length: " % QByteArray::number(body.size()) % "\r\n"); + payload.append("\r\n"); + + if (withBody) + payload.append(body); + + socket->write(payload); + if (withConnectionClose) + socket->disconnectFromHost(); + } + + static QLatin1StringView getHeaderValue(const QLatin1StringView keyHeader, + const QByteArrayView fullHeader) + { + const auto fullHeaderView = QLatin1StringView(fullHeader); + const qsizetype headerStart = fullHeaderView.indexOf(keyHeader, 0, Qt::CaseInsensitive); + if (headerStart == -1) + return {}; + qsizetype valueStart = headerStart + keyHeader.size(); + Q_ASSERT(fullHeaderView.size() > valueStart); + Q_ASSERT(fullHeaderView[valueStart] == ':'); + ++valueStart; + const qsizetype valueEnd = fullHeaderView.indexOf(QLatin1StringView("\r\n"), valueStart); + if (valueEnd == -1) + return {}; + return fullHeaderView.sliced(valueStart, valueEnd - valueStart).trimmed(); + } + + static QByteArray expectedBasicPayload() + { + return QByteArray(user % ':' % password).toBase64(); + } + + static constexpr QByteArrayView user = "user"; + static constexpr QByteArrayView password = "password"; + static constexpr QUtf8StringView body = "Authorization required"; + + bool withBody = false; + bool withContentLength = true; + bool withConnectionClose = false; + bool withEncryption = false; +#if QT_CONFIG(ssl) + QSslConfiguration configuration; +#endif + +private: + QByteArray incomingData; + bool finished = false; +}; + +struct ServerScenario { + QByteArrayView label; + bool withContentLength = false; + bool withBody = false; + bool withConnectionClose = false; + bool withEncryption = false; +}; +struct Credentials { QString username, password; }; +struct ClientScenario { + QByteArrayView label; + Credentials urlCredentials; + QVector<Credentials> callbackCredentials; + bool expectSuccess = true; +}; + +void tst_QWebSocket::authenticationRequired_data() +{ + const QString correctUser = QString::fromUtf8(AuthServer::user.toByteArray()); + const QString correctPassword = QString::fromUtf8(AuthServer::password.toByteArray()); + + QTest::addColumn<ServerScenario>("serverScenario"); + QTest::addColumn<ClientScenario>("clientScenario"); + + // Need to test multiple server scenarios: + // 1. Normal server (connection: keep-alive, Content-Length) + // 2. Older server (connection: close, Content-Length) + // 3. Even older server (connection: close, no Content-Length) + // 4. Strange server (connection: close, no Content-Length, no body) + // 5. Quiet server (connection: keep-alive, no Content-Length, no body) + ServerScenario serverScenarios[] = { + { "normal-server", true, true, false, false }, + { "connection-close", true, true, true, false }, + { "connection-close-no-content-length", false, true, true, false }, + { "connection-close-no-content-length-no-body", false, false, true, false }, + { "keep-alive-no-content-length-no-body", false, false, false, false }, + }; + + // And some client scenarios + // 1. User/pass supplied in url + // 2. User/pass supplied in callback + // 3. _Wrong_ user/pass supplied in URL, correct in callback + // 4. _Wrong_ user/pass supplied in URL, _wrong_ supplied in callback + // 5. No user/pass supplied in URL, nothing supplied in callback + // 5. No user/pass supplied in URL, wrong, then correct, supplied in callback + ClientScenario clientScenarios[]{ + { "url-ok", {correctUser, correctPassword}, {} }, + { "callback-ok", {}, { {correctUser, correctPassword } } }, + { "url-wrong-callback-ok", {u"admin"_s, u"admin"_s}, { {correctUser, correctPassword} } }, + { "url-wrong-callback-wrong", {u"admin"_s, u"admin"_s}, { {u"test"_s, u"test"_s} }, false }, + { "no-creds", {{}, {}}, {}, false }, + { "url-wrong-callback-2-ok", {u"admin"_s, u"admin"_s}, { {u"test"_s, u"test"_s}, {correctUser , correctPassword} } }, + }; + + for (auto &server : serverScenarios) { + for (auto &client : clientScenarios) { + QTest::addRow("Server:%s,Client:%s", server.label.data(), client.label.data()) + << server << client; + } + } +#if QT_CONFIG(ssl) + if (!QSslSocket::supportsSsl()) { + qDebug("Skipping the SslServer part of this test because proper TLS is not supported."); + return; + } + // And double that, but now with TLS + for (auto &server : serverScenarios) { + server.withEncryption = true; + for (auto &client : clientScenarios) { + QTest::addRow("SslServer:%s,Client:%s", server.label.data(), client.label.data()) + << server << client; + } + } +#endif +} + +void tst_QWebSocket::authenticationRequired() +{ + QFETCH(const ServerScenario, serverScenario); + QFETCH(const ClientScenario, clientScenario); + + int credentialIndex = 0; + auto handleAuthenticationRequired = [&clientScenario, + &credentialIndex](QAuthenticator *authenticator) { + if (credentialIndex == clientScenario.callbackCredentials.size()) { + if (clientScenario.expectSuccess) + QFAIL("Ran out of credentials to try, but failed to authorize!"); + if (clientScenario.callbackCredentials.isEmpty()) + return; + // If we don't expect to succeed, retry the last returned credentials. + // QAuthenticator should notice there is no change in user/pass and + // ignore it, leading to authentication failure. + --credentialIndex; + } + // Verify that realm parsing works: + QCOMPARE_EQ(authenticator->realm(), u"shadow"_s); + + Credentials credentials = clientScenario.callbackCredentials[credentialIndex++]; + authenticator->setUser(credentials.username); + authenticator->setPassword(credentials.password); + }; + + AuthServer server; + server.withBody = serverScenario.withBody; + server.withContentLength = serverScenario.withContentLength; + server.withConnectionClose = serverScenario.withConnectionClose; + server.withEncryption = serverScenario.withEncryption; +#if QT_CONFIG(ssl) + if (serverScenario.withEncryption) { + QSslConfiguration config = QSslConfiguration::defaultConfiguration(); + QList<QSslCertificate> certificates = QSslCertificate::fromPath(u":/localhost.cert"_s); + QVERIFY(!certificates.isEmpty()); + config.setLocalCertificateChain(certificates); + QFile keyFile(u":/localhost.key"_s); + QVERIFY(keyFile.open(QIODevice::ReadOnly)); + config.setPrivateKey(QSslKey(keyFile.readAll(), QSsl::Rsa)); + server.configuration = config; + } +#endif + + QVERIFY(server.listen()); + QUrl url = QUrl(u"ws://127.0.0.1"_s); + if (serverScenario.withEncryption) + url.setScheme(u"wss"_s); + url.setPort(server.serverPort()); + url.setUserName(clientScenario.urlCredentials.username); + url.setPassword(clientScenario.urlCredentials.password); + + QWebSocket socket; + QSignalSpy connectedSpy(&socket, &QWebSocket::connected); + QSignalSpy errorSpy(&socket, &QWebSocket::errorOccurred); + QSignalSpy stateChangedSpy(&socket, &QWebSocket::stateChanged); + connect(&socket, &QWebSocket::authenticationRequired, &socket, handleAuthenticationRequired); +#if QT_CONFIG(ssl) + if (serverScenario.withEncryption) { + auto config = socket.sslConfiguration(); + config.setPeerVerifyMode(QSslSocket::VerifyNone); + socket.setSslConfiguration(config); + QObject::connect(&socket, &QWebSocket::sslErrors, &socket, + qOverload<>(&QWebSocket::ignoreSslErrors)); + } +#endif + socket.open(url); + + if (clientScenario.expectSuccess) { + // Wait for connected! + QTRY_COMPARE_EQ(connectedSpy.size(), 1); + QCOMPARE_EQ(errorSpy.size(), 0); + // connecting->connected + const int ExpectedStateChanges = 2; + QTRY_COMPARE_EQ(stateChangedSpy.size(), ExpectedStateChanges); + auto firstState = stateChangedSpy.at(0).front().value<QAbstractSocket::SocketState>(); + QCOMPARE_EQ(firstState, QAbstractSocket::ConnectingState); + auto secondState = stateChangedSpy.at(1).front().value<QAbstractSocket::SocketState>(); + QCOMPARE_EQ(secondState, QAbstractSocket::ConnectedState); + } else { + // Wait for error! + QTRY_COMPARE_EQ(errorSpy.size(), 1); + QCOMPARE_EQ(connectedSpy.size(), 0); + // connecting->unconnected + const int ExpectedStateChanges = 2; + QTRY_COMPARE_EQ(stateChangedSpy.size(), ExpectedStateChanges); + auto firstState = stateChangedSpy.at(0).front().value<QAbstractSocket::SocketState>(); + QCOMPARE_EQ(firstState, QAbstractSocket::ConnectingState); + auto secondState = stateChangedSpy.at(1).front().value<QAbstractSocket::SocketState>(); + QCOMPARE_EQ(secondState, QAbstractSocket::UnconnectedState); + } +} + void tst_QWebSocket::overlongCloseReason() { EchoServer echoServer;