Revision control
Copy as Markdown
// Copyright (C) 2016 Kurt Pattyn <pattyn.kurt@gmail.com>.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QRegularExpression>
#include <QString>
#include <QtTest>
#include <QtWebSockets/QWebSocket>
#include <QtWebSockets/QWebSocketHandshakeOptions>
#include <QtWebSockets/QWebSocketCorsAuthenticator>
#include <QtWebSockets/QWebSocketServer>
#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>
#include <QtNetwork/qsslsocket.h>
#endif
#include <QtTest/private/qtesthelpers_p.h>
#include <memory>
#include <utility>
QT_USE_NAMESPACE
Q_DECLARE_METATYPE(QWebSocketProtocol::Version)
using namespace Qt::StringLiterals;
class EchoServer : public QObject
{
Q_OBJECT
public:
explicit EchoServer(QObject *parent = nullptr,
quint64 maxAllowedIncomingMessageSize = QWebSocket::maxIncomingMessageSize(),
quint64 maxAllowedIncomingFrameSize = QWebSocket::maxIncomingFrameSize());
~EchoServer();
QHostAddress hostAddress() const { return m_pWebSocketServer->serverAddress(); }
quint16 port() const { return m_pWebSocketServer->serverPort(); }
Q_SIGNALS:
void newConnection(QUrl requestUrl);
void newConnection(QNetworkRequest request);
void originAuthenticationRequired(QWebSocketCorsAuthenticator* pAuthenticator);
private Q_SLOTS:
void onNewConnection();
void processTextMessage(QString message);
void processBinaryMessage(QByteArray message);
void socketDisconnected();
private:
QWebSocketServer *m_pWebSocketServer;
quint64 m_maxAllowedIncomingMessageSize;
quint64 m_maxAllowedIncomingFrameSize;
QList<QWebSocket *> m_clients;
};
EchoServer::EchoServer(QObject *parent, quint64 maxAllowedIncomingMessageSize, quint64 maxAllowedIncomingFrameSize) :
QObject(parent),
m_pWebSocketServer(new QWebSocketServer(QStringLiteral("Echo Server"),
QWebSocketServer::NonSecureMode, this)),
m_maxAllowedIncomingMessageSize(maxAllowedIncomingMessageSize),
m_maxAllowedIncomingFrameSize(maxAllowedIncomingFrameSize),
m_clients()
{
m_pWebSocketServer->setSupportedSubprotocols({ QStringLiteral("protocol1"),
QStringLiteral("protocol2") });
if (m_pWebSocketServer->listen(QHostAddress(QStringLiteral("127.0.0.1")))) {
connect(m_pWebSocketServer, SIGNAL(newConnection()),
this, SLOT(onNewConnection()));
connect(m_pWebSocketServer, &QWebSocketServer::originAuthenticationRequired,
this, &EchoServer::originAuthenticationRequired);
}
}
EchoServer::~EchoServer()
{
m_pWebSocketServer->close();
qDeleteAll(m_clients.begin(), m_clients.end());
}
void EchoServer::onNewConnection()
{
QWebSocket *pSocket = m_pWebSocketServer->nextPendingConnection();
pSocket->setMaxAllowedIncomingFrameSize(m_maxAllowedIncomingFrameSize);
pSocket->setMaxAllowedIncomingMessageSize(m_maxAllowedIncomingMessageSize);
Q_EMIT newConnection(pSocket->requestUrl());
Q_EMIT newConnection(pSocket->request());
connect(pSocket, SIGNAL(textMessageReceived(QString)), this, SLOT(processTextMessage(QString)));
connect(pSocket, SIGNAL(binaryMessageReceived(QByteArray)), this, SLOT(processBinaryMessage(QByteArray)));
connect(pSocket, SIGNAL(disconnected()), this, SLOT(socketDisconnected()));
m_clients << pSocket;
}
void EchoServer::processTextMessage(QString message)
{
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
if (pClient) {
pClient->sendTextMessage(message);
}
}
void EchoServer::processBinaryMessage(QByteArray message)
{
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
if (pClient) {
pClient->sendBinaryMessage(message);
}
}
void EchoServer::socketDisconnected()
{
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
if (pClient) {
m_clients.removeAll(pClient);
pClient->deleteLater();
}
}
class tst_QWebSocket : public QObject
{
Q_OBJECT
public:
tst_QWebSocket();
private Q_SLOTS:
void init();
void initTestCase();
void cleanupTestCase();
void tst_initialisation_data();
void tst_initialisation();
void tst_settersAndGetters();
void tst_invalidOpen_data();
void tst_invalidOpen();
void tst_invalidOrigin();
void tst_sendTextMessage();
void tst_sendBinaryMessage();
void tst_ping();
void tst_errorString();
void tst_openRequest_data();
void tst_openRequest();
void tst_protocolAccessor();
void protocolsHeaderGeneration_data();
void protocolsHeaderGeneration();
void tst_moveToThread();
void tst_moveToThreadNoWarning();
#ifndef QT_NO_NETWORKPROXY
void tst_setProxy();
#endif
void authenticationRequired_data();
void authenticationRequired();
void overlongCloseReason();
void incomingMessageTooLong();
void incomingFrameTooLong();
void testingFrameAndMessageSizeApi();
void hostHeaderFromNetworkRequest();
void customHeader();
void splitUtf8Sequence();
void fragmentedServerHandshake_data();
void fragmentedServerHandshake();
void oversizedServerHandshake_data();
void oversizedServerHandshake();
void handshakeAbortedThenReopened_data();
void handshakeAbortedThenReopened();
void handshakeFollowedByFrame();
void reopenDoesNotReuseAuthenticator();
void reopenDoesNotSkipResponseBytes();
};
tst_QWebSocket::tst_QWebSocket()
{
}
void tst_QWebSocket::init()
{
qRegisterMetaType<QWebSocketProtocol::Version>("QWebSocketProtocol::Version");
}
void tst_QWebSocket::initTestCase()
{
}
void tst_QWebSocket::cleanupTestCase()
{
}
void tst_QWebSocket::tst_initialisation_data()
{
QTest::addColumn<QString>("origin");
QTest::addColumn<QString>("expectedOrigin");
QTest::addColumn<QWebSocketProtocol::Version>("version");
QTest::addColumn<QWebSocketProtocol::Version>("expectedVersion");
QTest::newRow("Default origin and version")
<< QString() << QString()
<< QWebSocketProtocol::VersionUnknown << QWebSocketProtocol::VersionLatest;
QTest::newRow("Specific origin and default version")
<< QStringLiteral("qt-project.org") << QStringLiteral("qt-project.org")
<< QWebSocketProtocol::VersionUnknown << QWebSocketProtocol::VersionLatest;
QTest::newRow("Specific origin and specific version")
<< QStringLiteral("qt-project.org") << QStringLiteral("qt-project.org")
<< QWebSocketProtocol::Version7 << QWebSocketProtocol::Version7;
}
void tst_QWebSocket::tst_initialisation()
{
QFETCH(QString, origin);
QFETCH(QString, expectedOrigin);
QFETCH(QWebSocketProtocol::Version, version);
QFETCH(QWebSocketProtocol::Version, expectedVersion);
QScopedPointer<QWebSocket> socket;
if (origin.isEmpty() && (version == QWebSocketProtocol::VersionUnknown))
socket.reset(new QWebSocket);
else if (!origin.isEmpty() && (version == QWebSocketProtocol::VersionUnknown))
socket.reset(new QWebSocket(origin));
else
socket.reset(new QWebSocket(origin, version));
QCOMPARE(socket->origin(), expectedOrigin);
QCOMPARE(socket->version(), expectedVersion);
QCOMPARE(socket->error(), QAbstractSocket::UnknownSocketError);
QVERIFY(socket->errorString().isEmpty());
QVERIFY(!socket->isValid());
QVERIFY(socket->localAddress().isNull());
QCOMPARE(socket->localPort(), quint16(0));
QCOMPARE(socket->pauseMode(), QAbstractSocket::PauseNever);
QVERIFY(socket->peerAddress().isNull());
QCOMPARE(socket->peerPort(), quint16(0));
QVERIFY(socket->peerName().isEmpty());
QCOMPARE(socket->state(), QAbstractSocket::UnconnectedState);
QCOMPARE(socket->readBufferSize(), 0);
QVERIFY(socket->resourceName().isEmpty());
QVERIFY(!socket->requestUrl().isValid());
QCOMPARE(socket->closeCode(), QWebSocketProtocol::CloseCodeNormal);
QVERIFY(socket->closeReason().isEmpty());
QVERIFY(socket->flush());
QCOMPARE(socket->sendTextMessage(QStringLiteral("A text message")), 0);
QCOMPARE(socket->sendBinaryMessage(QByteArrayLiteral("A binary message")), 0);
}
void tst_QWebSocket::tst_settersAndGetters()
{
QWebSocket socket;
socket.setPauseMode(QAbstractSocket::PauseNever);
QCOMPARE(socket.pauseMode(), QAbstractSocket::PauseNever);
socket.setPauseMode(QAbstractSocket::PauseOnSslErrors);
QCOMPARE(socket.pauseMode(), QAbstractSocket::PauseOnSslErrors);
socket.setReadBufferSize(0);
QCOMPARE(socket.readBufferSize(), 0);
socket.setReadBufferSize(128);
QCOMPARE(socket.readBufferSize(), 128);
socket.setReadBufferSize(-1);
QCOMPARE(socket.readBufferSize(), -1);
}
void tst_QWebSocket::tst_invalidOpen_data()
{
QTest::addColumn<QString>("url");
QTest::addColumn<QString>("expectedUrl");
QTest::addColumn<QString>("expectedPeerName");
QTest::addColumn<QString>("expectedResourceName");
QTest::addColumn<QAbstractSocket::SocketState>("stateAfterOpenCall");
QTest::addColumn<int>("disconnectedCount");
QTest::addColumn<int>("stateChangedCount");
QTest::newRow("Illegal local address")
<< QStringLiteral("127.0.0.1")
<< QStringLiteral("/") << QAbstractSocket::ConnectingState
<< 1
<< 2; //going from connecting to disconnected
QTest::newRow("URL containing new line in the hostname")
<< QString()
<< QString() << QAbstractSocket::UnconnectedState
<< 0 << 0;
QTest::newRow("URL containing new line in the resource name")
<< QString()
<< QString()
<< QAbstractSocket::UnconnectedState
<< 0 << 0;
}
void tst_QWebSocket::tst_invalidOpen()
{
QFETCH(QString, url);
QFETCH(QString, expectedUrl);
QFETCH(QString, expectedPeerName);
QFETCH(QString, expectedResourceName);
QFETCH(QAbstractSocket::SocketState, stateAfterOpenCall);
QFETCH(int, disconnectedCount);
QFETCH(int, stateChangedCount);
#if defined(__QNX__) && __QNX__ >= 800
QSKIP("QNX 8.0 rejects loopback connects to non-listening ports "
"synchronously, bypassing the ConnectingState transient the "
"test expects to observe.");
#endif
QWebSocket socket;
QSignalSpy errorSpy(&socket, SIGNAL(error(QAbstractSocket::SocketError)));
QSignalSpy aboutToCloseSpy(&socket, SIGNAL(aboutToClose()));
QSignalSpy connectedSpy(&socket, SIGNAL(connected()));
QSignalSpy disconnectedSpy(&socket, SIGNAL(disconnected()));
QSignalSpy stateChangedSpy(&socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)));
QSignalSpy readChannelFinishedSpy(&socket, SIGNAL(readChannelFinished()));
QSignalSpy textFrameReceivedSpy(&socket, SIGNAL(textFrameReceived(QString,bool)));
QSignalSpy binaryFrameReceivedSpy(&socket, SIGNAL(binaryFrameReceived(QByteArray,bool)));
QSignalSpy textMessageReceivedSpy(&socket, SIGNAL(textMessageReceived(QString)));
QSignalSpy binaryMessageReceivedSpy(&socket, SIGNAL(binaryMessageReceived(QByteArray)));
QSignalSpy pongSpy(&socket, SIGNAL(pong(quint64,QByteArray)));
QSignalSpy bytesWrittenSpy(&socket, SIGNAL(bytesWritten(qint64)));
socket.open(QUrl(url));
QVERIFY(socket.origin().isEmpty());
QCOMPARE(socket.version(), QWebSocketProtocol::VersionLatest);
//at this point the socket is in a connecting state
//so, there should no error at this point
QCOMPARE(socket.error(), QAbstractSocket::UnknownSocketError);
QVERIFY(!socket.errorString().isEmpty());
QVERIFY(!socket.isValid());
QVERIFY(socket.localAddress().isNull());
QCOMPARE(socket.localPort(), quint16(0));
QCOMPARE(socket.pauseMode(), QAbstractSocket::PauseNever);
QVERIFY(socket.peerAddress().isNull());
QCOMPARE(socket.peerPort(), quint16(0));
QCOMPARE(socket.peerName(), expectedPeerName);
QCOMPARE(socket.state(), stateAfterOpenCall);
QCOMPARE(socket.readBufferSize(), 0);
QCOMPARE(socket.resourceName(), expectedResourceName);
QCOMPARE(socket.requestUrl().toString(), expectedUrl);
QCOMPARE(socket.closeCode(), QWebSocketProtocol::CloseCodeNormal);
QVERIFY(socket.closeReason().isEmpty());
QCOMPARE(socket.sendTextMessage(QStringLiteral("A text message")), 0);
QCOMPARE(socket.sendBinaryMessage(QByteArrayLiteral("A text message")), 0);
if (errorSpy.size() == 0)
QVERIFY(errorSpy.wait());
QCOMPARE(errorSpy.size(), 1);
QList<QVariant> arguments = errorSpy.takeFirst();
QAbstractSocket::SocketError socketError =
qvariant_cast<QAbstractSocket::SocketError>(arguments.at(0));
QCOMPARE(socketError, QAbstractSocket::ConnectionRefusedError);
QCOMPARE(aboutToCloseSpy.size(), 0);
QCOMPARE(connectedSpy.size(), 0);
QCOMPARE(disconnectedSpy.size(), disconnectedCount);
QCOMPARE(stateChangedSpy.size(), stateChangedCount);
if (stateChangedCount == 2) {
arguments = stateChangedSpy.takeFirst();
QAbstractSocket::SocketState socketState =
qvariant_cast<QAbstractSocket::SocketState>(arguments.at(0));
arguments = stateChangedSpy.takeFirst();
socketState = qvariant_cast<QAbstractSocket::SocketState>(arguments.at(0));
QCOMPARE(socketState, QAbstractSocket::UnconnectedState);
}
QCOMPARE(readChannelFinishedSpy.size(), 0);
QCOMPARE(textFrameReceivedSpy.size(), 0);
QCOMPARE(binaryFrameReceivedSpy.size(), 0);
QCOMPARE(textMessageReceivedSpy.size(), 0);
QCOMPARE(binaryMessageReceivedSpy.size(), 0);
QCOMPARE(pongSpy.size(), 0);
QCOMPARE(bytesWrittenSpy.size(), 0);
}
void tst_QWebSocket::tst_invalidOrigin()
{
#if defined(__QNX__) && __QNX__ >= 800
QSKIP("QNX 8.0 rejects loopback connects to non-listening ports "
"synchronously, bypassing the ConnectingState transient the "
"test expects to observe.");
#endif
QWebSocket socket(QStringLiteral("My server\r\nin the wild."));
QSignalSpy errorSpy(&socket, SIGNAL(error(QAbstractSocket::SocketError)));
QSignalSpy aboutToCloseSpy(&socket, SIGNAL(aboutToClose()));
QSignalSpy connectedSpy(&socket, SIGNAL(connected()));
QSignalSpy disconnectedSpy(&socket, SIGNAL(disconnected()));
QSignalSpy stateChangedSpy(&socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)));
QSignalSpy readChannelFinishedSpy(&socket, SIGNAL(readChannelFinished()));
QSignalSpy textFrameReceivedSpy(&socket, SIGNAL(textFrameReceived(QString,bool)));
QSignalSpy binaryFrameReceivedSpy(&socket, SIGNAL(binaryFrameReceived(QByteArray,bool)));
QSignalSpy textMessageReceivedSpy(&socket, SIGNAL(textMessageReceived(QString)));
QSignalSpy binaryMessageReceivedSpy(&socket, SIGNAL(binaryMessageReceived(QByteArray)));
QSignalSpy pongSpy(&socket, SIGNAL(pong(quint64,QByteArray)));
QSignalSpy bytesWrittenSpy(&socket, SIGNAL(bytesWritten(qint64)));
//at this point the socket is in a connecting state
//so, there should no error at this point
QCOMPARE(socket.error(), QAbstractSocket::UnknownSocketError);
QVERIFY(!socket.errorString().isEmpty());
QVERIFY(!socket.isValid());
QVERIFY(socket.localAddress().isNull());
QCOMPARE(socket.localPort(), quint16(0));
QCOMPARE(socket.pauseMode(), QAbstractSocket::PauseNever);
QVERIFY(socket.peerAddress().isNull());
QCOMPARE(socket.peerPort(), quint16(0));
QCOMPARE(socket.peerName(), QStringLiteral("127.0.0.1"));
QCOMPARE(socket.state(), QAbstractSocket::ConnectingState);
QCOMPARE(socket.readBufferSize(), 0);
QCOMPARE(socket.resourceName(), QStringLiteral("/"));
QCOMPARE(socket.closeCode(), QWebSocketProtocol::CloseCodeNormal);
QVERIFY(errorSpy.wait());
QCOMPARE(errorSpy.size(), 1);
QList<QVariant> arguments = errorSpy.takeFirst();
QAbstractSocket::SocketError socketError =
qvariant_cast<QAbstractSocket::SocketError>(arguments.at(0));
QCOMPARE(socketError, QAbstractSocket::ConnectionRefusedError);
QCOMPARE(aboutToCloseSpy.size(), 0);
QCOMPARE(connectedSpy.size(), 0);
QCOMPARE(disconnectedSpy.size(), 1);
QCOMPARE(stateChangedSpy.size(), 2); //connectingstate, unconnectedstate
arguments = stateChangedSpy.takeFirst();
QAbstractSocket::SocketState socketState =
qvariant_cast<QAbstractSocket::SocketState>(arguments.at(0));
arguments = stateChangedSpy.takeFirst();
socketState = qvariant_cast<QAbstractSocket::SocketState>(arguments.at(0));
QCOMPARE(socketState, QAbstractSocket::UnconnectedState);
QCOMPARE(readChannelFinishedSpy.size(), 0);
QCOMPARE(textFrameReceivedSpy.size(), 0);
QCOMPARE(binaryFrameReceivedSpy.size(), 0);
QCOMPARE(textMessageReceivedSpy.size(), 0);
QCOMPARE(binaryMessageReceivedSpy.size(), 0);
QCOMPARE(pongSpy.size(), 0);
QCOMPARE(bytesWrittenSpy.size(), 0);
}
void tst_QWebSocket::tst_sendTextMessage()
{
EchoServer echoServer;
QWebSocket socket;
//should return 0 because socket is not open yet
QCOMPARE(socket.sendTextMessage(QStringLiteral("1234")), 0);
QSignalSpy socketConnectedSpy(&socket, SIGNAL(connected()));
QSignalSpy serverConnectedSpy(&echoServer, SIGNAL(newConnection(QUrl)));
QSignalSpy textMessageReceived(&socket, SIGNAL(textMessageReceived(QString)));
QSignalSpy textFrameReceived(&socket, SIGNAL(textFrameReceived(QString,bool)));
QSignalSpy binaryMessageReceived(&socket, SIGNAL(binaryMessageReceived(QByteArray)));
QSignalSpy binaryFrameReceived(&socket, SIGNAL(binaryFrameReceived(QByteArray,bool)));
QSignalSpy socketError(&socket, SIGNAL(error(QAbstractSocket::SocketError)));
QUrl url = QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
QStringLiteral(":") + QString::number(echoServer.port()));
url.setPath("/segment/with spaces");
QUrlQuery query;
query.addQueryItem("queryitem", "with encoded characters");
url.setQuery(query);
socket.open(url);
QTRY_COMPARE(socketConnectedSpy.size(), 1);
QCOMPARE(socketError.size(), 0);
QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
QList<QVariant> arguments = serverConnectedSpy.takeFirst();
QUrl urlConnected = arguments.at(0).toUrl();
QCOMPARE(urlConnected, url);
QCOMPARE(socket.bytesToWrite(), 0);
socket.sendTextMessage(QStringLiteral("Hello world!"));
QVERIFY(socket.bytesToWrite() > 12); // 12 + a few extra bytes for header
QVERIFY(textMessageReceived.wait(500));
QCOMPARE(socket.bytesToWrite(), 0);
QCOMPARE(textMessageReceived.size(), 1);
QCOMPARE(binaryMessageReceived.size(), 0);
QCOMPARE(binaryFrameReceived.size(), 0);
arguments = textMessageReceived.takeFirst();
QString messageReceived = arguments.at(0).toString();
QCOMPARE(messageReceived, QStringLiteral("Hello world!"));
QCOMPARE(textFrameReceived.size(), 1);
arguments = textFrameReceived.takeFirst();
QString frameReceived = arguments.at(0).toString();
bool isLastFrame = arguments.at(1).toBool();
QCOMPARE(frameReceived, QStringLiteral("Hello world!"));
QVERIFY(isLastFrame);
socket.close();
socketConnectedSpy.clear();
textMessageReceived.clear();
textFrameReceived.clear();
// QTBUG-74464 QWebsocket doesn't receive text (binary) message with size > 32 kb
socket.open(url);
QTRY_COMPARE(socketConnectedSpy.size(), 1);
QCOMPARE(socketError.size(), 0);
QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
arguments = serverConnectedSpy.takeFirst();
urlConnected = arguments.at(0).toUrl();
QCOMPARE(urlConnected, url);
QCOMPARE(socket.bytesToWrite(), 0);
// transmit a long text message with 1 MB
QString longString(0x100000, 'a');
socket.sendTextMessage(longString);
QVERIFY(socket.bytesToWrite() > longString.size());
QVERIFY(textMessageReceived.wait());
QCOMPARE(socket.bytesToWrite(), 0);
QCOMPARE(textMessageReceived.size(), 1);
QCOMPARE(binaryMessageReceived.size(), 0);
QCOMPARE(binaryFrameReceived.size(), 0);
arguments = textMessageReceived.takeFirst();
messageReceived = arguments.at(0).toString();
QCOMPARE(messageReceived.size(), longString.size());
QCOMPARE(messageReceived, longString);
arguments = textFrameReceived.takeLast();
isLastFrame = arguments.at(1).toBool();
QVERIFY(isLastFrame);
socket.close();
socketConnectedSpy.clear();
textMessageReceived.clear();
textFrameReceived.clear();
//QTBUG-36762: QWebSocket emits multiplied signals when socket was reopened
socket.open(QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
QStringLiteral(":") + QString::number(echoServer.port())));
QTRY_COMPARE(socketConnectedSpy.size(), 1);
QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
socket.sendTextMessage(QStringLiteral("Hello world!"));
QVERIFY(textMessageReceived.wait(500));
QCOMPARE(textMessageReceived.size(), 1);
QCOMPARE(binaryMessageReceived.size(), 0);
QCOMPARE(binaryFrameReceived.size(), 0);
arguments = textMessageReceived.takeFirst();
messageReceived = arguments.at(0).toString();
QCOMPARE(messageReceived, QStringLiteral("Hello world!"));
QCOMPARE(textFrameReceived.size(), 1);
arguments = textFrameReceived.takeFirst();
frameReceived = arguments.at(0).toString();
isLastFrame = arguments.at(1).toBool();
QCOMPARE(frameReceived, QStringLiteral("Hello world!"));
QVERIFY(isLastFrame);
QString reason = QStringLiteral("going away");
socket.close(QWebSocketProtocol::CloseCodeGoingAway, reason);
QCOMPARE(socket.closeCode(), QWebSocketProtocol::CloseCodeGoingAway);
QCOMPARE(socket.closeReason(), reason);
}
void tst_QWebSocket::tst_sendBinaryMessage()
{
EchoServer echoServer;
QWebSocket socket;
//should return 0 because socket is not open yet
QCOMPARE(socket.sendBinaryMessage(QByteArrayLiteral("1234")), 0);
QSignalSpy socketConnectedSpy(&socket, SIGNAL(connected()));
QSignalSpy textMessageReceived(&socket, SIGNAL(textMessageReceived(QString)));
QSignalSpy textFrameReceived(&socket, SIGNAL(textFrameReceived(QString,bool)));
QSignalSpy binaryMessageReceived(&socket, SIGNAL(binaryMessageReceived(QByteArray)));
QSignalSpy binaryFrameReceived(&socket, SIGNAL(binaryFrameReceived(QByteArray,bool)));
socket.open(QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
QStringLiteral(":") + QString::number(echoServer.port())));
QTRY_COMPARE(socketConnectedSpy.size(), 1);
QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
QCOMPARE(socket.bytesToWrite(), 0);
socket.sendBinaryMessage(QByteArrayLiteral("Hello world!"));
QVERIFY(socket.bytesToWrite() > 12); // 12 + a few extra bytes for header
QVERIFY(binaryMessageReceived.wait(500));
QCOMPARE(socket.bytesToWrite(), 0);
QCOMPARE(textMessageReceived.size(), 0);
QCOMPARE(textFrameReceived.size(), 0);
QCOMPARE(binaryMessageReceived.size(), 1);
QList<QVariant> arguments = binaryMessageReceived.takeFirst();
QByteArray messageReceived = arguments.at(0).toByteArray();
QCOMPARE(messageReceived, QByteArrayLiteral("Hello world!"));
QCOMPARE(binaryFrameReceived.size(), 1);
arguments = binaryFrameReceived.takeFirst();
QByteArray frameReceived = arguments.at(0).toByteArray();
bool isLastFrame = arguments.at(1).toBool();
QCOMPARE(frameReceived, QByteArrayLiteral("Hello world!"));
QVERIFY(isLastFrame);
socket.close();
//QTBUG-36762: QWebSocket emits multiple signals when socket is reopened
socketConnectedSpy.clear();
binaryMessageReceived.clear();
binaryFrameReceived.clear();
socket.open(QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
QStringLiteral(":") + QString::number(echoServer.port())));
QTRY_COMPARE(socketConnectedSpy.size(), 1);
QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
socket.sendBinaryMessage(QByteArrayLiteral("Hello world!"));
QVERIFY(binaryMessageReceived.wait(500));
QCOMPARE(textMessageReceived.size(), 0);
QCOMPARE(textFrameReceived.size(), 0);
QCOMPARE(binaryMessageReceived.size(), 1);
arguments = binaryMessageReceived.takeFirst();
messageReceived = arguments.at(0).toByteArray();
QCOMPARE(messageReceived, QByteArrayLiteral("Hello world!"));
QCOMPARE(binaryFrameReceived.size(), 1);
arguments = binaryFrameReceived.takeFirst();
frameReceived = arguments.at(0).toByteArray();
isLastFrame = arguments.at(1).toBool();
QCOMPARE(frameReceived, QByteArrayLiteral("Hello world!"));
QVERIFY(isLastFrame);
}
void tst_QWebSocket::tst_ping()
{
EchoServer echoServer;
QWebSocket socket;
QSignalSpy socketConnectedSpy(&socket, &QWebSocket::connected);
QSignalSpy serverConnectedSpy(&echoServer, QOverload<QUrl>::of(&EchoServer::newConnection));
QSignalSpy pongReceived(&socket, &QWebSocket::pong);
QSignalSpy textMessageReceived(&socket, &QWebSocket::textMessageReceived);
QSignalSpy textFrameReceived(&socket, &QWebSocket::textFrameReceived);
QSignalSpy binaryMessageReceived(&socket, &QWebSocket::binaryMessageReceived);
QSignalSpy binaryFrameReceived(&socket, &QWebSocket::binaryFrameReceived);
QSignalSpy socketError(&socket, &QWebSocket::errorOccurred);
socket.ping(QByteArrayLiteral("Hello ping!")); // ping as no return value
QVERIFY(!pongReceived.wait(500));
QCOMPARE(pongReceived.size(), 0); // But we should have no pong result
QUrl url = QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
QStringLiteral(":") + QString::number(echoServer.port()));
url.setPath("/segment/with spaces");
QUrlQuery query;
query.addQueryItem("queryitem", "with encoded characters");
url.setQuery(query);
socket.open(url);
QTRY_COMPARE(socketConnectedSpy.size(), 1);
QCOMPARE(socketError.size(), 0);
QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
QList<QVariant> arguments = serverConnectedSpy.takeFirst();
QUrl urlConnected = arguments.at(0).toUrl();
QCOMPARE(urlConnected, url);
QCOMPARE(socket.bytesToWrite(), 0);
socket.ping(QByteArrayLiteral("Hello ping!"));
QVERIFY(socket.bytesToWrite() > 11); // 11 + a few extra bytes for header
QVERIFY(pongReceived.wait(500));
QCOMPARE(socket.bytesToWrite(), 0);
QCOMPARE(pongReceived.size(), 1);
QCOMPARE(textMessageReceived.size(), 0);
QCOMPARE(textFrameReceived.size(), 0);
QCOMPARE(binaryMessageReceived.size(), 0);
QCOMPARE(binaryFrameReceived.size(), 0);
arguments = pongReceived.takeFirst();
QString messageReceived = arguments.at(1).toString();
bool elapsedIsInt;
quint64 elapsed = arguments.at(0).toUInt(&elapsedIsInt);
QCOMPARE(elapsedIsInt, true);
QCOMPARE_LT(elapsed, 1000);
QCOMPARE(messageReceived, QByteArrayLiteral("Hello ping!"));
QByteArray longByteArray(125, 'a');
longByteArray.append(125, 'b');
socket.ping(longByteArray); // ping method should send only 125 first bytes
QVERIFY(socket.bytesToWrite() > 125); // 125 + a few extra bytes for header
QVERIFY(pongReceived.wait(500));
QCOMPARE(socket.bytesToWrite(), 0);
QCOMPARE(pongReceived.size(), 1);
QCOMPARE(textMessageReceived.size(), 0);
QCOMPARE(textFrameReceived.size(), 0);
QCOMPARE(binaryMessageReceived.size(), 0);
QCOMPARE(binaryFrameReceived.size(), 0);
arguments = pongReceived.takeFirst();
messageReceived = arguments.at(1).toString();
elapsed = arguments.at(0).toUInt(&elapsedIsInt);
QCOMPARE(elapsedIsInt, true);
QCOMPARE_LT(elapsed, 1000);
QCOMPARE(messageReceived, longByteArray.left(125)); // max length is of 125 for ping
QCOMPARE(messageReceived[124], 'a'); // and we should still be in 'a' part
socket.close();
socketConnectedSpy.clear();
textMessageReceived.clear();
textFrameReceived.clear();
pongReceived.clear();
}
void tst_QWebSocket::tst_errorString()
{
//Check for QTBUG-37228: QWebSocket returns "Unknown Error" for known errors
QWebSocket socket;
//check that the default error string is empty
QVERIFY(socket.errorString().isEmpty());
QSignalSpy errorSpy(&socket, SIGNAL(error(QAbstractSocket::SocketError)));
QTRY_COMPARE_WITH_TIMEOUT(errorSpy.size(), 1, 10000);
QList<QVariant> arguments = errorSpy.takeFirst();
QAbstractSocket::SocketError socketError =
qvariant_cast<QAbstractSocket::SocketError>(arguments.at(0));
QCOMPARE(socketError, QAbstractSocket::HostNotFoundError);
QCOMPARE(socket.errorString(), QStringLiteral("Host not found"));
// Check that handshake status code is parsed. The error is triggered by
// refusing the origin authentication
EchoServer echoServer;
errorSpy.clear();
QSignalSpy socketConnectedSpy(&socket, SIGNAL(connected()));
QSignalSpy serverConnectedSpy(&echoServer, SIGNAL(newConnection(QUrl)));
connect(&echoServer, &EchoServer::originAuthenticationRequired,
&socket, [](QWebSocketCorsAuthenticator* pAuthenticator){
pAuthenticator->setAllowed(false);
});
socket.open(QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
QStringLiteral(":") + QString::number(echoServer.port())));
QTRY_VERIFY(errorSpy.size() > 0);
QCOMPARE(serverConnectedSpy.size(), 0);
QCOMPARE(socketConnectedSpy.size(), 0);
QCOMPARE(socket.errorString(),
QStringLiteral("QWebSocketPrivate::processHandshake: Unhandled http status code: 403"
" (Access Forbidden)."));
}
void tst_QWebSocket::tst_openRequest_data()
{
QTest::addColumn<QStringList>("subprotocols");
QTest::addColumn<QString>("subprotocolHeader");
QTest::addColumn<QRegularExpression>("warningExpression");
QTest::addRow("no subprotocols") << QStringList{} << QString{} << QRegularExpression{};
QTest::addRow("single subprotocol") << QStringList{"foobar"} << QStringLiteral("foobar")
<< QRegularExpression{};
QTest::addRow("multiple subprotocols") << QStringList{"foo", "bar"}
<< QStringLiteral("foo, bar")
<< QRegularExpression{};
QTest::addRow("subprotocol with whitespace")
<< QStringList{"chat", "foo\r\nbar with space"}
<< QStringLiteral("chat")
<< QRegularExpression{".*invalid.*bar with space"};
QTest::addRow("subprotocol with invalid chars")
<< QStringList{"chat", "foo{}"}
<< QStringLiteral("chat")
<< QRegularExpression{".*invalid.*foo"};
}
void tst_QWebSocket::tst_openRequest()
{
QFETCH(QStringList, subprotocols);
QFETCH(QString, subprotocolHeader);
QFETCH(QRegularExpression, warningExpression);
EchoServer echoServer;
QWebSocket socket;
QSignalSpy socketConnectedSpy(&socket, SIGNAL(connected()));
QSignalSpy serverRequestSpy(&echoServer, SIGNAL(newConnection(QNetworkRequest)));
QUrl url = QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
QLatin1Char(':') + QString::number(echoServer.port()));
url.setPath(QLatin1String("/"));
QUrlQuery query;
query.addQueryItem("queryitem", "with encoded characters");
url.setQuery(query);
QNetworkRequest req(url);
req.setRawHeader("X-Custom-Header", "A custom header");
QWebSocketHandshakeOptions options;
options.setSubprotocols(subprotocols);
if (!warningExpression.pattern().isEmpty())
QTest::ignoreMessage(QtWarningMsg, warningExpression);
socket.open(req, options);
QTRY_COMPARE(socketConnectedSpy.size(), 1);
QTRY_COMPARE(serverRequestSpy.size(), 1);
QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
QList<QVariant> arguments = serverRequestSpy.takeFirst();
QNetworkRequest requestConnected = arguments.at(0).value<QNetworkRequest>();
QCOMPARE(requestConnected.url(), req.url());
QCOMPARE(requestConnected.rawHeader("X-Custom-Header"), req.rawHeader("X-Custom-Header"));
if (subprotocols.isEmpty())
QVERIFY(!requestConnected.hasRawHeader("Sec-WebSocket-Protocol"));
else
QCOMPARE(requestConnected.rawHeader("Sec-WebSocket-Protocol"), subprotocolHeader);
socket.close();
}
void tst_QWebSocket::tst_protocolAccessor()
{
EchoServer echoServer;
QWebSocket socket;
QUrl url = QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
QLatin1Char(':') + QString::number(echoServer.port()));
QWebSocketHandshakeOptions options;
options.setSubprotocols({ "foo", "protocol2" });
socket.open(url, options);
QTRY_COMPARE(socket.state(), QAbstractSocket::ConnectedState);
QCOMPARE(socket.subprotocol(), "protocol2");
socket.close();
}
void tst_QWebSocket::protocolsHeaderGeneration_data()
{
QTest::addColumn<QStringList>("subprotocols");
QTest::addColumn<int>("numInvalidEntries");
using QSL = QStringList;
QTest::addRow("all-invalid") << QSL{ "hello?", "------,,,,------" } << 2;
QTest::addRow("one-valid") << QSL{ "hello?", "ImValid" } << 1;
QTest::addRow("all-valid") << QSL{ "hello", "ImValid" } << 0;
}
// We test that the Sec-WebSocket-Protocol header is generated normally in presence
// of one or more invalid entries. That is, it should not be included at all
// if there are no valid entries, and there should be no separators with only
// one valid entry.
void tst_QWebSocket::protocolsHeaderGeneration()
{
QFETCH(const QStringList, subprotocols);
QFETCH(const int, numInvalidEntries);
const bool containsValidEntry = numInvalidEntries != subprotocols.size();
QTcpServer tcpServer;
QVERIFY(tcpServer.listen());
QWebSocket socket;
QWebSocketHandshakeOptions options;
options.setSubprotocols(subprotocols);
QCOMPARE(options.subprotocols().size(), subprotocols.size());
for (int i = 0; i < numInvalidEntries; ++i) {
QTest::ignoreMessage(QtMsgType::QtWarningMsg,
QRegularExpression("Ignoring invalid WebSocket subprotocol name \".*\""));
}
socket.open(url, options);
QTRY_VERIFY(tcpServer.hasPendingConnections());
QTcpSocket *serverSocket = tcpServer.nextPendingConnection();
QVERIFY(serverSocket);
bool hasSeenHeader = false;
while (serverSocket->state() == QAbstractSocket::ConnectedState) {
if (!serverSocket->canReadLine()) {
QTRY_VERIFY2(serverSocket->canReadLine(),
"Reached end-of-data without seeing end-of-header!");
}
const QByteArray fullLine = serverSocket->readLine();
QByteArrayView line = fullLine;
if (line == "\r\n") // End-of-Header
break;
QByteArrayView headerPrefix = "Sec-WebSocket-Protocol:";
if (line.size() < headerPrefix.size())
continue;
if (line.first(headerPrefix.size()).compare(headerPrefix, Qt::CaseInsensitive) != 0)
continue;
hasSeenHeader = true;
QByteArrayView protocols = line.sliced(headerPrefix.size()).trimmed();
QVERIFY(!protocols.empty());
QCOMPARE(protocols.count(','), subprotocols.size() - numInvalidEntries - 1);
// Keep going in case we encounter the header again
}
QCOMPARE(hasSeenHeader, containsValidEntry);
serverSocket->disconnectFromHost();
}
class WebSocket : public QWebSocket
{
Q_OBJECT
public:
explicit WebSocket()
{
connect(this, SIGNAL(triggerClose()), SLOT(onClose()), Qt::QueuedConnection);
connect(this, SIGNAL(triggerOpen(QUrl)), SLOT(onOpen(QUrl)), Qt::QueuedConnection);
connect(this, SIGNAL(triggerSendTextMessage(QString)), SLOT(onSendTextMessage(QString)), Qt::QueuedConnection);
connect(this, SIGNAL(textMessageReceived(QString)), this, SLOT(onTextMessageReceived(QString)), Qt::QueuedConnection);
}
void asyncClose() { triggerClose(); }
void asyncOpen(const QUrl &url) { triggerOpen(url); }
void asyncSendTextMessage(const QString &msg) { triggerSendTextMessage(msg); }
QString receivedMessage;
Q_SIGNALS:
void triggerClose();
void triggerOpen(const QUrl &);
void triggerSendTextMessage(const QString &);
void done();
private Q_SLOTS:
void onClose() { close(); }
void onOpen(const QUrl &url) { open(url); }
void onSendTextMessage(const QString &msg) { sendTextMessage(msg); }
void onTextMessageReceived(const QString &msg) { receivedMessage = msg; done(); }
};
struct Warned
{
static QtMessageHandler origHandler;
static bool warned;
static void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& str)
{
if (type == QtWarningMsg) {
warned = true;
}
if (origHandler)
origHandler(type, context, str);
}
};
QtMessageHandler Warned::origHandler = nullptr;
bool Warned::warned = false;
void tst_QWebSocket::tst_moveToThread()
{
Warned::origHandler = qInstallMessageHandler(&Warned::messageHandler);
EchoServer echoServer;
QThread* thread = new QThread(this);
thread->start();
WebSocket* socket = new WebSocket;
socket->moveToThread(thread);
const QString textMessage = QStringLiteral("Hello world!");
QSignalSpy socketConnectedSpy(socket, SIGNAL(connected()));
QUrl url = QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
QStringLiteral(":") + QString::number(echoServer.port()));
url.setPath("/segment/with spaces");
QUrlQuery query;
query.addQueryItem("queryitem", "with encoded characters");
url.setQuery(query);
socket->asyncOpen(url);
if (socketConnectedSpy.size() == 0)
QVERIFY(socketConnectedSpy.wait(500));
socket->asyncSendTextMessage(textMessage);
QTimer timer;
timer.setInterval(1000);
timer.start();
QEventLoop loop;
connect(socket, SIGNAL(done()), &loop, SLOT(quit()));
connect(socket, SIGNAL(done()), &timer, SLOT(stop()));
connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
loop.exec();
socket->asyncClose();
QTRY_COMPARE_WITH_TIMEOUT(loop.isRunning(), false, 200);
QCOMPARE(socket->receivedMessage, textMessage);
socket->deleteLater();
thread->quit();
thread->wait();
}
void tst_QWebSocket::tst_moveToThreadNoWarning()
{
// check for warnings in tst_moveToThread()
// couldn't done there because warnings are processed after the test run
QCOMPARE(Warned::warned, false);
}
#ifndef QT_NO_NETWORKPROXY
void tst_QWebSocket::tst_setProxy()
{
// check if property assignment works as expected.
QWebSocket socket;
QCOMPARE(socket.proxy(), QNetworkProxy(QNetworkProxy::DefaultProxy));
QNetworkProxy proxy;
proxy.setPort(123);
socket.setProxy(proxy);
QCOMPARE(socket.proxy(), proxy);
proxy.setPort(321);
QCOMPARE(socket.proxy().port(), quint16(123));
socket.setProxy(proxy);
QCOMPARE(socket.proxy(), proxy);
}
#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->close();
}
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;
}
if (QTestPrivate::isSecureTransportBlockingTest()) {
qDebug("SecureTransport is blocking in keychain access.");
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());
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;
QWebSocket socket;
//should return 0 because socket is not open yet
QCOMPARE(socket.sendTextMessage(QStringLiteral("1234")), 0);
QSignalSpy socketConnectedSpy(&socket, SIGNAL(connected()));
QSignalSpy socketDisconnectedSpy(&socket, SIGNAL(disconnected()));
QSignalSpy serverConnectedSpy(&echoServer, SIGNAL(newConnection(QUrl)));
QUrl url = QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
QStringLiteral(":") + QString::number(echoServer.port()));
socket.open(url);
QTRY_COMPARE(socketConnectedSpy.size(), 1);
QTRY_COMPARE(serverConnectedSpy.size(), 1);
const QString reason(200, QChar::fromLatin1('a'));
socket.close(QWebSocketProtocol::CloseCodeGoingAway, reason);
QCOMPARE(socket.closeCode(), QWebSocketProtocol::CloseCodeGoingAway);
// Max length of a control frame is 125, but 2 bytes are used for the close code:
QCOMPARE(socket.closeReason().size(), 123);
QCOMPARE(socket.closeReason(), reason.left(123));
QTRY_COMPARE(socketDisconnectedSpy.size(), 1);
}
void tst_QWebSocket::incomingMessageTooLong()
{
//QTBUG-70693
quint64 maxAllowedIncomingMessageSize = 1024;
quint64 maxAllowedIncomingFrameSize = QWebSocket::maxIncomingFrameSize();
EchoServer echoServer(nullptr, maxAllowedIncomingMessageSize, maxAllowedIncomingFrameSize);
QWebSocket socket;
QSignalSpy socketConnectedSpy(&socket, &QWebSocket::connected);
QSignalSpy serverConnectedSpy(&echoServer, QOverload<QUrl>::of(&EchoServer::newConnection));
QSignalSpy socketDisconnectedSpy(&socket, &QWebSocket::disconnected);
QUrl url = QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
QStringLiteral(":") + QString::number(echoServer.port()));
socket.open(url);
QTRY_COMPARE(socketConnectedSpy.size(), 1);
QTRY_COMPARE(serverConnectedSpy.size(), 1);
QString payload(maxAllowedIncomingMessageSize+1, 'a');
QCOMPARE(socket.sendTextMessage(payload), payload.size());
QTRY_COMPARE(socketDisconnectedSpy.size(), 1);
QCOMPARE(socket.closeCode(), QWebSocketProtocol::CloseCodeTooMuchData);
}
void tst_QWebSocket::incomingFrameTooLong()
{
//QTBUG-70693
quint64 maxAllowedIncomingMessageSize = QWebSocket::maxIncomingMessageSize();
quint64 maxAllowedIncomingFrameSize = 1024;
EchoServer echoServer(nullptr, maxAllowedIncomingMessageSize, maxAllowedIncomingFrameSize);
QWebSocket socket;
socket.setOutgoingFrameSize(maxAllowedIncomingFrameSize+1);
QSignalSpy socketConnectedSpy(&socket, &QWebSocket::connected);
QSignalSpy serverConnectedSpy(&echoServer, QOverload<QUrl>::of(&EchoServer::newConnection));
QSignalSpy socketDisconnectedSpy(&socket, &QWebSocket::disconnected);
QUrl url = QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
QStringLiteral(":") + QString::number(echoServer.port()));
socket.open(url);
QTRY_COMPARE(socketConnectedSpy.size(), 1);
QTRY_COMPARE(serverConnectedSpy.size(), 1);
QString payload(maxAllowedIncomingFrameSize+1, 'a');
QCOMPARE(socket.sendTextMessage(payload), payload.size());
QTRY_COMPARE(socketDisconnectedSpy.size(), 1);
QCOMPARE(socket.closeCode(), QWebSocketProtocol::CloseCodeTooMuchData);
}
void tst_QWebSocket::testingFrameAndMessageSizeApi()
{
//requested by André Hartmann, QTBUG-70693
QWebSocket socket;
const quint64 outgoingFrameSize = 5;
socket.setOutgoingFrameSize(outgoingFrameSize);
QTRY_COMPARE(outgoingFrameSize, socket.outgoingFrameSize());
const quint64 maxAllowedIncomingFrameSize = 9;
socket.setMaxAllowedIncomingFrameSize(maxAllowedIncomingFrameSize);
QTRY_COMPARE(maxAllowedIncomingFrameSize, socket.maxAllowedIncomingFrameSize());
const quint64 maxAllowedIncomingMessageSize = 889;
socket.setMaxAllowedIncomingMessageSize(maxAllowedIncomingMessageSize);
QTRY_COMPARE(maxAllowedIncomingMessageSize, socket.maxAllowedIncomingMessageSize());
}
void tst_QWebSocket::hostHeaderFromNetworkRequest()
{
QTcpServer server;
QSignalSpy serverSpy(&server, &QTcpServer::newConnection);
QVERIFY(server.listen(QHostAddress(u"127.0.0.1"_s)));
url.setPort(server.serverPort());
QNetworkRequest request(url);
request.setRawHeader("Host", "localhost");
QWebSocket socket;
socket.open(request);
QVERIFY(serverSpy.wait());
std::unique_ptr<QTcpSocket> serverSocket;
serverSocket.reset(server.nextPendingConnection());
QSignalSpy serverSocketSpy(serverSocket.get(), &QIODevice::readyRead);
QByteArray data;
while (!data.contains("\r\n\r\n")) {
QVERIFY(serverSocketSpy.wait());
data.append(serverSocket->readAll());
}
const auto headerEndIndex = data.indexOf("\r\n\r\n");
QCOMPARE_NE(headerEndIndex, -1);
const auto headerView = QLatin1StringView(QByteArrayView(data).first(headerEndIndex + 2));
QCOMPARE(headerView.count("Host: "_L1, Qt::CaseInsensitive), 1);
QVERIFY(headerView.contains("\r\nHost: localhost\r\n"_L1, Qt::CaseInsensitive));
const QLatin1StringView keyView =
AuthServer::getHeaderValue("Sec-WebSocket-Key"_L1, QByteArrayView(data));
QVERIFY(!keyView.isEmpty());
const QByteArray accept =
QByteArrayView(keyView) % "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"_ba;
serverSocket->write(
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: "
% QCryptographicHash::hash(accept, QCryptographicHash::Sha1).toBase64()
% "\r\n\r\n");
QSignalSpy connectedSpy(&socket, &QWebSocket::connected);
QVERIFY(connectedSpy.wait());
}
void tst_QWebSocket::customHeader()
{
QTcpServer server;
QSignalSpy serverSpy(&server, &QTcpServer::newConnection);
server.listen();
url.setPort(server.serverPort());
QNetworkRequest request(url);
request.setRawHeader("CustomHeader", "Example");
QWebSocket socket;
socket.open(request);
// Custom websocket server below (needed because a QWebSocketServer on
// localhost doesn't show the issue):
QVERIFY(serverSpy.wait());
QTcpSocket *serverSocket = server.nextPendingConnection();
QSignalSpy serverSocketSpy(serverSocket, &QTcpSocket::readyRead);
QByteArray data;
while (!data.contains("\r\n\r\n")) {
QVERIFY(serverSocketSpy.wait());
data.append(serverSocket->readAll());
}
QVERIFY(QLatin1StringView(data).contains("CustomHeader: Example"_L1, Qt::CaseInsensitive));
const auto view = QLatin1String(data);
const auto keyHeader = QLatin1String("Sec-WebSocket-Key:");
const qsizetype keyStart = view.indexOf(keyHeader, 0, Qt::CaseInsensitive) + keyHeader.size();
QVERIFY(keyStart != -1);
const qsizetype keyEnd = view.indexOf(QLatin1String("\r\n"), keyStart);
QVERIFY(keyEnd != -1);
const QLatin1String keyView = view.sliced(keyStart, keyEnd - keyStart).trimmed();
const QByteArray accept =
QByteArrayView(keyView) % QByteArrayLiteral("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
serverSocket->write(
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: "
% QCryptographicHash::hash(accept, QCryptographicHash::Sha1).toBase64()
); // trailing \r\n\r\n intentionally left off to make the client wait for it
serverSocket->flush();
// This would freeze prior to the fix for QTBUG-102111, because the client would loop forever.
// We use qWait to give the OS some time to move the bytes over to the client and push the event
// to our eventloop.
QTest::qWait(100);
serverSocket->write("\r\n\r\n");
// And check the client properly connects:
QSignalSpy connectedSpy(&socket, &QWebSocket::connected);
QVERIFY(connectedSpy.wait());
}
void tst_QWebSocket::splitUtf8Sequence()
{
EchoServer echoServer;
QWebSocket socket;
socket.setOutgoingFrameSize(10ull);
// prepare payload, just a bunch of bytes and a sequence at the end that will be split across
// two frames
QString payload;
constexpr qsizetype Overhead = 2; // overhead for this message
payload.reserve(qsizetype(socket.outgoingFrameSize() - Overhead) + 2);
payload.assign(qsizetype(socket.outgoingFrameSize() - Overhead) - 1, u'a');
payload += u"🙂";
QSignalSpy socketConnectedSpy(&socket, &QWebSocket::connected);
QSignalSpy serverConnectedSpy(&echoServer, qOverload<QUrl>(&EchoServer::newConnection));
QSignalSpy messageReceivedSpy(&socket, &QWebSocket::textMessageReceived);
QSignalSpy errorOccurredSpy(&socket, &QWebSocket::errorOccurred);
socket.open(url);
QTRY_COMPARE(socketConnectedSpy.size(), 1);
QTRY_COMPARE(serverConnectedSpy.size(), 1);
qint64 bytesWritten = socket.sendTextMessage(payload);
QCOMPARE(bytesWritten, payload.size() + Overhead);
QTRY_COMPARE(messageReceivedSpy.size(), 1);
QTRY_COMPARE(errorOccurredSpy.size(), 0);
QCOMPARE(messageReceivedSpy.at(0).at(0).toString(), payload);
}
// Reads one complete request header off the socket and returns it.
static QByteArray readRequestHeader(QTcpSocket *serverSocket)
{
// Spin the event loop rather than blocking on the socket: the client lives in this
// same thread, and would never get to send its request.
QSignalSpy readyReadSpy(serverSocket, &QTcpSocket::readyRead);
QByteArray data;
while (!data.contains("\r\n\r\n")) {
if (serverSocket->bytesAvailable() == 0 && !readyReadSpy.wait(5000))
return {};
data.append(serverSocket->readAll());
}
return data;
}
// Reads the client's request off the socket and returns the matching 101 response.
static QByteArray upgradeResponseFor(QTcpSocket *serverSocket)
{
const QByteArray data = readRequestHeader(serverSocket);
if (data.isEmpty())
return {};
const auto view = QLatin1String(data);
const auto keyHeader = QLatin1String("Sec-WebSocket-Key:");
const qsizetype keyStart = view.indexOf(keyHeader, 0, Qt::CaseInsensitive);
if (keyStart == -1)
return {};
const qsizetype valueStart = keyStart + keyHeader.size();
const qsizetype valueEnd = view.indexOf(QLatin1String("\r\n"), valueStart);
if (valueEnd == -1)
return {};
const QLatin1String keyView = view.sliced(valueStart, valueEnd - valueStart).trimmed();
const QByteArray accept =
QByteArrayView(keyView) % "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"_ba;
return "HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: "_ba
% QCryptographicHash::hash(accept, QCryptographicHash::Sha1).toBase64()
% "\r\n\r\n"_ba;
}
void tst_QWebSocket::fragmentedServerHandshake_data()
{
// Bytes withheld from the bulk write, so the terminator is split at each offset.
QTest::addColumn<int>("withheldTail");
QTest::newRow("one byte at a time") << 0;
for (int i = 1; i < 4; ++i)
QTest::addRow("terminator split %d", i) << i;
}
void tst_QWebSocket::fragmentedServerHandshake()
{
QFETCH(const int, withheldTail);
QTcpServer tcpServer;
QVERIFY(tcpServer.listen(QHostAddress::LocalHost));
QWebSocket socket;
QSignalSpy connectedSpy(&socket, &QWebSocket::connected);
QSignalSpy errorSpy(&socket, &QWebSocket::errorOccurred);
QTRY_VERIFY(tcpServer.hasPendingConnections());
QTcpSocket *serverSocket = tcpServer.nextPendingConnection();
QVERIFY(serverSocket);
const QByteArray response = upgradeResponseFor(serverSocket);
QVERIFY(!response.isEmpty());
// The client must not consider itself connected until the terminator arrives,
// wherever the write boundaries fall.
qsizetype sent = 0;
while (sent < response.size()) {
qsizetype size = 1; // one byte at a time
if (withheldTail != 0)
size = sent ? withheldTail : response.size() - withheldTail;
if (sent + size == response.size()) {
// Everything but the tail has been delivered. Running the event loop
// must not produce a connection, neither now nor earlier.
QVERIFY(!connectedSpy.wait(100));
QCOMPARE(connectedSpy.size(), 0);
}
QCOMPARE(serverSocket->write(response.constData() + sent, size), size);
while (serverSocket->bytesToWrite())
QVERIFY(serverSocket->waitForBytesWritten());
sent += size;
}
QTRY_COMPARE(connectedSpy.size(), 1);
QCOMPARE(errorSpy.size(), 0);
}
void tst_QWebSocket::oversizedServerHandshake_data()
{
QTest::addColumn<bool>("complete");
QTest::newRow("no terminator") << false;
// A response whose headers are complete has to be rejected on size too, not
// parsed just because the terminator was already there to be found.
QTest::newRow("complete headers") << true;
}
void tst_QWebSocket::oversizedServerHandshake()
{
QFETCH(const bool, complete);
QTcpServer tcpServer;
QVERIFY(tcpServer.listen(QHostAddress::LocalHost));
QWebSocket socket;
QSignalSpy connectedSpy(&socket, &QWebSocket::connected);
QSignalSpy errorSpy(&socket, &QWebSocket::errorOccurred);
QTRY_VERIFY(tcpServer.hasPendingConnections());
QTcpSocket *serverSocket = tcpServer.nextPendingConnection();
QVERIFY(serverSocket);
QVERIFY(!upgradeResponseFor(serverSocket).isEmpty()); // consume the request
// Answer with a response padded to exactly the header ceiling of 100 lines of
// 8 KiB plus the terminator. The limit itself has to be refused.
constexpr qsizetype limit = 100 * 8 * 1024 + 4;
constexpr qsizetype terminatorSize = 4;
QByteArray response = "HTTP/1.1 101 Switching Protocols\r\nX-Padding: ";
const qsizetype paddingSize = limit - response.size() - (complete ? terminatorSize : 0);
response += QByteArray(paddingSize, 'x');
if (complete)
response += "\r\n\r\n";
QCOMPARE(response.size(), limit);
QCOMPARE(serverSocket->write(response), response.size());
QTRY_COMPARE(errorSpy.size(), 1);
QCOMPARE(errorSpy.first().first().value<QAbstractSocket::SocketError>(),
QAbstractSocket::ConnectionRefusedError);
QCOMPARE(socket.errorString(), "Header is too large."_L1);
QCOMPARE(socket.closeCode(), QWebSocketProtocol::CloseCodeTooMuchData);
QCOMPARE(connectedSpy.size(), 0);
// The client has to hang up rather than keep buffering the response.
QTRY_COMPARE(serverSocket->state(), QAbstractSocket::UnconnectedState);
}
void tst_QWebSocket::handshakeAbortedThenReopened_data()
{
QTest::addColumn<QByteArray>("partialResponse");
// Without a newline the client cannot start parsing, so nothing is retained.
QTest::newRow("no complete line") << QByteArray("HTTP/1.1 101");
// With one complete line the client starts accumulating, and whatever it kept
// must not leak into the next connection.
QTest::newRow("one complete line")
<< QByteArray("HTTP/1.1 101 Switching Protocols\r\nUpgrade: web");
}
void tst_QWebSocket::handshakeAbortedThenReopened()
{
QFETCH(const QByteArray, partialResponse);
QTcpServer tcpServer;
QVERIFY(tcpServer.listen(QHostAddress::LocalHost));
QWebSocket socket;
QSignalSpy connectedSpy(&socket, &QWebSocket::connected);
// First attempt: the server answers with only part of the response, then hangs up.
socket.open(url);
QTRY_VERIFY(tcpServer.hasPendingConnections());
{
const std::unique_ptr<QTcpSocket> serverSocket(tcpServer.nextPendingConnection());
QVERIFY(serverSocket);
QVERIFY(!upgradeResponseFor(serverSocket.get()).isEmpty());
QCOMPARE(serverSocket->write(partialResponse), partialResponse.size());
while (serverSocket->bytesToWrite())
QVERIFY(serverSocket->waitForBytesWritten());
serverSocket->disconnectFromHost();
}
QTRY_COMPARE(socket.state(), QAbstractSocket::UnconnectedState);
QCOMPARE(connectedSpy.size(), 0);
// Second attempt on the same QWebSocket: the abandoned partial response must not
// be carried over and prepended to this one.
socket.open(url);
QTRY_VERIFY(tcpServer.hasPendingConnections());
const std::unique_ptr<QTcpSocket> serverSocket(tcpServer.nextPendingConnection());
QVERIFY(serverSocket);
const QByteArray response = upgradeResponseFor(serverSocket.get());
QVERIFY(!response.isEmpty());
QCOMPARE(serverSocket->write(response), response.size());
QTRY_COMPARE(connectedSpy.size(), 1);
}
void tst_QWebSocket::handshakeFollowedByFrame()
{
QTcpServer tcpServer;
QVERIFY(tcpServer.listen(QHostAddress::LocalHost));
QWebSocket socket;
QSignalSpy connectedSpy(&socket, &QWebSocket::connected);
QSignalSpy errorSpy(&socket, &QWebSocket::errorOccurred);
QTRY_VERIFY(tcpServer.hasPendingConnections());
const std::unique_ptr<QTcpSocket> serverSocket(tcpServer.nextPendingConnection());
QVERIFY(serverSocket);
const QByteArray response = upgradeResponseFor(serverSocket.get());
QVERIFY(!response.isEmpty());
// Start an unmasked text frame in the same write as the response, so reading the
// response must not consume the frame bytes that follow the terminator. The frame
// is left one byte short until the message spy exists, so it cannot be missed.
const QByteArray partialFrame = QByteArray::fromHex("810268");
QCOMPARE(serverSocket->write(response + partialFrame), response.size() + partialFrame.size());
QTRY_COMPARE(connectedSpy.size(), 1);
QSignalSpy messageSpy(&socket, &QWebSocket::textMessageReceived);
QCOMPARE(serverSocket->write(QByteArray::fromHex("69")), 1);
QTRY_COMPARE(messageSpy.size(), 1);
QCOMPARE(messageSpy.first().first().toString(), "hi"_L1);
QCOMPARE(errorSpy.size(), 0);
}
// Answers a handshake request with a 401 Basic challenge under the given realm, and
// returns the request that was answered.
static QByteArray answerWithAuthChallenge(QTcpSocket *serverSocket, QByteArrayView realm,
QByteArrayView extraHeaders = {})
{
const QByteArray request = readRequestHeader(serverSocket);
if (request.isEmpty())
return {};
QByteArray response = "HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm=";
response.append(realm);
response.append("\r\n");
response.append(extraHeaders);
response.append("\r\n");
if (serverSocket->write(response) != response.size())
return {};
return request;
}
// The decoded "user:password" a request offers, so a failure names the credential that
// was actually sent rather than its base64.
static QByteArray basicCredentialIn(QByteArrayView request)
{
const QLatin1StringView value = AuthServer::getHeaderValue("Authorization"_L1, request);
if (!value.startsWith("Basic "_L1))
return {};
return QByteArray::fromBase64(QByteArrayView(value.sliced(6)).toByteArray());
}
// A QWebSocket reused for a second connection must not carry the first connection's
// QAuthenticator with it: the credential the application produced for one host is not
// the credential it would produce for another, and it must be asked again.
void tst_QWebSocket::reopenDoesNotReuseAuthenticator()
{
QTcpServer hostA;
QVERIFY(hostA.listen(QHostAddress::LocalHost));
QTcpServer hostB;
QVERIFY(hostB.listen(QHostAddress::LocalHost));
// A per-host secret, as any real credential store would return.
const QStringList secrets = { u"secret-for-host-a"_s, u"secret-for-host-b"_s };
int authRequests = 0;
QWebSocket socket;
connect(&socket, &QWebSocket::authenticationRequired, &socket,
[&](QAuthenticator *authenticator) {
authenticator->setUser(u"user"_s);
authenticator->setPassword(secrets.value(authRequests, u"unexpected"_s));
++authRequests;
});
// First connection: host A challenges, the application supplies host A's secret,
// and the client resends authenticated. All of this is correct.
socket.open(urlA);
QTRY_VERIFY(hostA.hasPendingConnections());
{
const std::unique_ptr<QTcpSocket> serverSocket(hostA.nextPendingConnection());
QVERIFY(serverSocket);
QVERIFY(!answerWithAuthChallenge(serverSocket.get(), "host-a").isEmpty());
const QByteArray authenticated = readRequestHeader(serverSocket.get());
QVERIFY(!authenticated.isEmpty());
QCOMPARE(authRequests, 1);
QCOMPARE(basicCredentialIn(authenticated), "user:secret-for-host-a");
// Host A goes away before the exchange completes, leaving the authenticator
// holding host A's credentials.
serverSocket->disconnectFromHost();
}
QTRY_COMPARE(socket.state(), QAbstractSocket::UnconnectedState);
// Second connection, same QWebSocket, different host and different realm.
socket.open(urlB);
QTRY_VERIFY(hostB.hasPendingConnections());
const std::unique_ptr<QTcpSocket> serverSocket(hostB.nextPendingConnection());
QVERIFY(serverSocket);
const QByteArray opening = answerWithAuthChallenge(serverSocket.get(), "host-b");
QVERIFY(!opening.isEmpty());
// Nothing has challenged us yet, so the opening request carries no credential.
QCOMPARE(basicCredentialIn(opening), QByteArray());
// Host B's challenge must reach the application before any credential reaches
// host B. Without that, the carried-over authenticator answers on its own and
// host B is handed the secret that was only ever meant for host A.
const QByteArray answered = readRequestHeader(serverSocket.get());
QVERIFY(!answered.isEmpty());
QCOMPARE(basicCredentialIn(answered), "user:secret-for-host-b");
QCOMPARE(authRequests, 2);
}
// A QWebSocket reused for a second connection must not carry the first connection's
// pending body-skip count with it, or it eats the front of the next server's response.
void tst_QWebSocket::reopenDoesNotSkipResponseBytes()
{
QTcpServer hostA;
QVERIFY(hostA.listen(QHostAddress::LocalHost));
QTcpServer hostB;
QVERIFY(hostB.listen(QHostAddress::LocalHost));
QWebSocket socket;
connect(&socket, &QWebSocket::authenticationRequired, &socket,
[](QAuthenticator *authenticator) {
authenticator->setUser(u"user"_s);
authenticator->setPassword(u"password"_s);
});
// First connection: host A announces a 4 KiB error body with its challenge but
// never sends it, so the client is still waiting to skip those bytes.
socket.open(urlA);
QTRY_VERIFY(hostA.hasPendingConnections());
{
const std::unique_ptr<QTcpSocket> serverSocket(hostA.nextPendingConnection());
QVERIFY(serverSocket);
QVERIFY(!answerWithAuthChallenge(serverSocket.get(), "host-a",
"Content-Length: 4096\r\n").isEmpty());
QVERIFY(!readRequestHeader(serverSocket.get()).isEmpty()); // the resend
serverSocket->disconnectFromHost();
}
QTRY_COMPARE(socket.state(), QAbstractSocket::UnconnectedState);
// Second connection, same QWebSocket: a well-behaved server completes a clean
// handshake, which has to be parsed from its very first byte.
QSignalSpy connectedSpy(&socket, &QWebSocket::connected);
QSignalSpy errorSpy(&socket, &QWebSocket::errorOccurred);
socket.open(urlB);
QTRY_VERIFY(hostB.hasPendingConnections());
const std::unique_ptr<QTcpSocket> serverSocket(hostB.nextPendingConnection());
QVERIFY(serverSocket);
const QByteArray response = upgradeResponseFor(serverSocket.get());
QVERIFY(!response.isEmpty());
QCOMPARE(serverSocket->write(response), response.size());
QTRY_COMPARE(connectedSpy.size(), 1);
QCOMPARE(errorSpy.size(), 0);
}
QTEST_MAIN(tst_QWebSocket)
#include "tst_qwebsocket.moc"