summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMÃ¥rten Nordheim <marten.nordheim@qt.io>2022-12-12 15:25:20 +0100
committerTimur Pocheptsov <timur.pocheptsov@qt.io>2022-12-21 15:30:54 +0100
commitda30f70fea239f723f1d36b076bb3f5860f50ed9 (patch)
treed3e7abdd6f860fcb5963a457655f00bef77ef65e
parent482034a8f25f02662df0a2558b7f771f46cec142 (diff)
downloadqtwebsockets-da30f70fea239f723f1d36b076bb3f5860f50ed9.tar.gz
Support 401 response for websocket connections
Adds the authenticationRequired signal. [ChangeLog][QWebSocket] QWebSocket now supports 401 Unauthorized. Connect to the authenticationRequired signal to handle authentication challenges, or pass your credentials along with the URL. Fixes: QTBUG-92858 Change-Id: Ic43d1c12529dea278b2951e6f991cc1004fc3713 Reviewed-by: Timur Pocheptsov <timur.pocheptsov@qt.io> Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
-rw-r--r--src/websockets/qwebsocket.cpp21
-rw-r--r--src/websockets/qwebsocket.h1
-rw-r--r--src/websockets/qwebsocket_p.cpp107
-rw-r--r--src/websockets/qwebsocket_p.h10
-rw-r--r--tests/auto/websockets/qwebsocket/CMakeLists.txt13
-rw-r--r--tests/auto/websockets/qwebsocket/tst_qwebsocket.cpp310
6 files changed, 460 insertions, 2 deletions
diff --git a/src/websockets/qwebsocket.cpp b/src/websockets/qwebsocket.cpp
index 93d07f4..b495ffd 100644
--- a/src/websockets/qwebsocket.cpp
+++ b/src/websockets/qwebsocket.cpp
@@ -98,6 +98,27 @@ not been filled in with new information when the signal returns.
\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 --git a/src/websockets/qwebsocket.h b/src/websockets/qwebsocket.h
index 5cc6131..bf9e393 100644
--- a/src/websockets/qwebsocket.h
+++ b/src/websockets/qwebsocket.h
@@ -118,6 +118,7 @@ Q_SIGNALS:
#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 --git a/src/websockets/qwebsocket_p.cpp b/src/websockets/qwebsocket_p.cpp
index 08c170e..bca6d4f 100644
--- a/src/websockets/qwebsocket_p.cpp
+++ b/src/websockets/qwebsocket_p.cpp
@@ -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 int MAX_HEADERLINE_LENGTH = 8 * 1024; // maximum length of a http request header line
@@ -1080,6 +1084,51 @@ void QWebSocketPrivate::processHandshake(QTcpSocket *pSocket)
}
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) {
+ // Keep the error on a single line so it can easily be searched for:
+ errorDescription =
+ QWebSocket::tr("QWebSocketPrivate::processHandshake: "
+ "Unsupported WWW-Authenticate challenge(s) encountered!");
+ 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).")
@@ -1092,6 +1141,16 @@ void QWebSocketPrivate::processHandshake(QTcpSocket *pSocket)
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);
@@ -1130,6 +1189,27 @@ void QWebSocketPrivate::processStateChanged(QAbstractSocket::SocketState socketS
}
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;
@@ -1156,7 +1236,28 @@ void QWebSocketPrivate::processStateChanged(QAbstractSocket::SocketState socketS
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();
}
@@ -1187,7 +1288,9 @@ void QWebSocketPrivate::processData()
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 --git a/src/websockets/qwebsocket_p.h b/src/websockets/qwebsocket_p.h
index 08be774..f29b40a 100644
--- a/src/websockets/qwebsocket_p.h
+++ b/src/websockets/qwebsocket_p.h
@@ -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>
@@ -205,12 +206,21 @@ private:
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 --git a/tests/auto/websockets/qwebsocket/CMakeLists.txt b/tests/auto/websockets/qwebsocket/CMakeLists.txt
index 25e61e8..a70d61b 100644
--- a/tests/auto/websockets/qwebsocket/CMakeLists.txt
+++ b/tests/auto/websockets/qwebsocket/CMakeLists.txt
@@ -14,5 +14,18 @@ qt_internal_add_test(tst_qwebsocket
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 --git a/tests/auto/websockets/qwebsocket/tst_qwebsocket.cpp b/tests/auto/websockets/qwebsocket/tst_qwebsocket.cpp
index ea5c2b1..3fb79ec 100644
--- a/tests/auto/websockets/qwebsocket/tst_qwebsocket.cpp
+++ b/tests/auto/websockets/qwebsocket/tst_qwebsocket.cpp
@@ -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 @@ private Q_SLOTS:
#ifndef QT_NO_NETWORKPROXY
void tst_setProxy();
#endif
+ void authenticationRequired_data();
+ void authenticationRequired();
void overlongCloseReason();
void incomingMessageTooLong();
void incomingFrameTooLong();
@@ -921,6 +933,304 @@ void tst_QWebSocket::tst_setProxy()
}
#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";
+ const bool connectionClose = withBody && !withContentLength;
+ if (connectionClose)
+ 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 (connectionClose)
+ 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 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 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:
+ // (note: Connection: Close is modelled as 'no Content-Length, with body')
+ // 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 },
+ { "connection-close", true, true, false },
+ { "connection-close-no-content-length", false, true, false },
+ { "connection-close-no-content-length-no-body", false, false, false },
+ { "keep-alive-no-content-length-no-body", 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 0 //QT_CONFIG(ssl)
+ // FIXME: Consider TLS-shutdown different from Schannel.
+ // 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.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;