/*! * \brief Unit tests for \ref RemoteConnector * * \copyright Copyright (c) 2017-2018 Governikus GmbH & Co. KG, Germany */ #include "RemoteConnectorImpl.h" #include "AppSettings.h" #include "Env.h" #include "KeyPair.h" #include "messages/Discovery.h" #include "RemoteWebSocketServer.h" #include "SecureStorage.h" #include #include #include #include using namespace governikus; Q_DECLARE_METATYPE(RemoteDeviceDescriptor) Q_DECLARE_METATYPE(QSharedPointer ) Q_DECLARE_METATYPE(RemoteErrorCode) class test_RemoteConnector : public QObject { Q_OBJECT private: void sendRequest(const QSharedPointer& pConnector, const QHostAddress& pHostAddress, const Discovery& pDiscovery, const QString& pPassword) { const RemoteDeviceDescriptor descr(pDiscovery, pHostAddress); QMetaObject::invokeMethod(pConnector.data(), [ = ] { pConnector->onConnectRequest(descr, pPassword); }, Qt::QueuedConnection); } void verifySuccessSignal(const QSignalSpy& pSignalSpy, const quint16 pPort) { const static QHostAddress HOST_ADDRESS(QHostAddress::LocalHost); const static QString IFD_NAME("Smartphone1"); bool signalFound = false; for (const QList& arguments : pSignalSpy) { const QVariant remoteDeviceDescriptorVariant = arguments.at(0); QVERIFY(remoteDeviceDescriptorVariant.canConvert()); const RemoteDeviceDescriptor descr = remoteDeviceDescriptorVariant.value(); QVERIFY(!descr.isNull()); const QVariant dispatcherVariant = arguments.at(1); QVERIFY(dispatcherVariant.canConvert >()); const QSharedPointer dispatcher = dispatcherVariant.value >(); QVERIFY(dispatcher); const QUrl remoteUrl = descr.getUrl(); const QString remoteAddress = remoteUrl.host(); const int remotePort = remoteUrl.port(); const bool signalMatches = remoteAddress == HOST_ADDRESS.toString() && remotePort == pPort && descr.getIfdName() == IFD_NAME; if (signalMatches) { qDebug() << "Success verified"; signalFound = true; break; } } QVERIFY(signalFound); } void verifyErrorSignal(const QSignalSpy& pSignalSpy, const QList& pRemoteErrorCodeList, const quint16 pPort, const QString& pIfdName, const bool pExpectingNullDeviceDescriptor = false) { const static QHostAddress HOST_ADDRESS(QHostAddress::LocalHost); bool signalFound = false; for (const QList& arguments : pSignalSpy) { const QVariant remoteDeviceDescriptorVariant = arguments.at(0); QVERIFY(remoteDeviceDescriptorVariant.canConvert()); const RemoteDeviceDescriptor descr = remoteDeviceDescriptorVariant.value(); if (pExpectingNullDeviceDescriptor && descr.isNull()) { qDebug() << "Error verified"; signalFound = true; break; } QVERIFY(!descr.isNull()); const QVariant errorCodeVariant = arguments.at(1); QVERIFY(errorCodeVariant.canConvert()); const RemoteErrorCode errorCode = errorCodeVariant.value(); const QUrl remoteUrl = descr.getUrl(); const QString remoteAddress = remoteUrl.host(); const int remotePort = remoteUrl.port(); const bool signalMatches = remoteAddress == HOST_ADDRESS.toString() && remotePort == pPort && descr.getIfdName() == pIfdName && pRemoteErrorCodeList.contains(errorCode); if (signalMatches) { qDebug() << "Error verified"; signalFound = true; break; } } QVERIFY(signalFound); } private Q_SLOTS: void initTestCase() { qRegisterMetaType("QWebSocketProtocol::CloseCode"); } void requestWithNoDeviceNameIsRejected() { QThread clientThread; const QSharedPointer connector(new RemoteConnectorImpl()); QSignalSpy spyError(connector.data(), &RemoteConnector::fireRemoteDispatcherError); QSignalSpy spySuccess(connector.data(), &RemoteConnector::fireRemoteDispatcherCreated); connector->moveToThread(&clientThread); clientThread.start(); // No device name. const QHostAddress hostAddress(QHostAddress::LocalHost); const Discovery discoveryMsg(QString(), QStringLiteral("0123456789ABCDEF"), 2020, {IfdVersion::Version::v0}); sendRequest(connector, hostAddress, discoveryMsg, QString()); QTRY_COMPARE(spyError.count(), 1); clientThread.exit(); QVERIFY(clientThread.wait()); verifyErrorSignal(spyError, {RemoteErrorCode::INVALID_REQUEST}, 2020, QString()); QCOMPARE(spySuccess.count(), 0); } void requestWithNoDeviceInfoIsRejected() { QThread clientThread; const QSharedPointer connector(new RemoteConnectorImpl()); QSignalSpy spyError(connector.data(), &RemoteConnector::fireRemoteDispatcherError); QSignalSpy spySuccess(connector.data(), &RemoteConnector::fireRemoteDispatcherCreated); connector->moveToThread(&clientThread); clientThread.start(); // Device information is null. const QHostAddress hostAddress(QHostAddress::LocalHost); sendRequest(connector, hostAddress, Discovery(QJsonObject()), QStringLiteral("secret")); QTRY_COMPARE(spyError.count(), 1); clientThread.exit(); QVERIFY(clientThread.wait()); verifyErrorSignal(spyError, {RemoteErrorCode::INVALID_REQUEST}, 0, QString(), true); QCOMPARE(spySuccess.count(), 0); } void requestWithEncryptedServerAndReconnectFails() { QScopedPointer server(Env::create()); server->listen("dummy"); QThread clientThread; const QSharedPointer connector(new RemoteConnectorImpl()); QSignalSpy spyError(connector.data(), &RemoteConnector::fireRemoteDispatcherError); QSignalSpy spySuccess(connector.data(), &RemoteConnector::fireRemoteDispatcherCreated); connector->moveToThread(&clientThread); clientThread.start(); // Password is empty. const QHostAddress hostAddress(QHostAddress::LocalHost); const Discovery discoveryMsg(QStringLiteral("Smartphone1"), QStringLiteral("0123456789ABCDEF"), server->getServerPort(), {IfdVersion::Version::v0}); sendRequest(connector, hostAddress, discoveryMsg, QString()); QTRY_COMPARE(spyError.count(), 1); clientThread.exit(); QVERIFY(clientThread.wait()); verifyErrorSignal(spyError, {RemoteErrorCode::REMOTE_HOST_REFUSED_CONNECTION}, server->getServerPort(), QStringLiteral("Smartphone1")); QCOMPARE(spySuccess.count(), 0); } void requestWithUnsupportedApiLevelIsRejected() { QThread clientThread; const QSharedPointer connector(new RemoteConnectorImpl()); QSignalSpy spyError(connector.data(), &RemoteConnector::fireRemoteDispatcherError); QSignalSpy spySuccess(connector.data(), &RemoteConnector::fireRemoteDispatcherCreated); connector->moveToThread(&clientThread); clientThread.start(); // Currently, only API level 1 is supported. const QHostAddress hostAddress(QHostAddress::LocalHost); const Discovery discoveryMsg(QStringLiteral("Smartphone1"), QStringLiteral("0123456789ABCDEF"), 2020, {IfdVersion::Version::v_test}); sendRequest(connector, hostAddress, discoveryMsg, QStringLiteral("secret")); QTRY_COMPARE(spyError.count(), 1); clientThread.exit(); QVERIFY(clientThread.wait()); verifyErrorSignal(spyError, {RemoteErrorCode::NO_SUPPORTED_API_LEVEL}, 2020, QStringLiteral("Smartphone1")); QCOMPARE(spySuccess.count(), 0); } void noServerResponseCausesConnectionErrorOrTimeout() { QThread clientThread; const QSharedPointer connector(new RemoteConnectorImpl(2500)); QSignalSpy spyError(connector.data(), &RemoteConnector::fireRemoteDispatcherError); QSignalSpy spySuccess(connector.data(), &RemoteConnector::fireRemoteDispatcherCreated); connector->moveToThread(&clientThread); clientThread.start(); // Correct request but no server is running. const QHostAddress hostAddress(QHostAddress::LocalHost); const Discovery discoveryMsg(QStringLiteral("Smartphone1"), QStringLiteral("0123456789ABCDEF"), 2020, {IfdVersion::Version::v0}); sendRequest(connector, hostAddress, discoveryMsg, QString("dummy")); QTRY_COMPARE(spyError.count(), 1); clientThread.exit(); QVERIFY(clientThread.wait()); #if !defined(Q_OS_FREEBSD) && !defined(Q_OS_WIN) verifyErrorSignal(spyError, {RemoteErrorCode::CONNECTION_ERROR}, 2020, QStringLiteral("Smartphone1")); #else verifyErrorSignal(spyError, {RemoteErrorCode::CONNECTION_TIMEOUT, RemoteErrorCode::CONNECTION_ERROR}, 2020, QStringLiteral("Smartphone1")); #endif QCOMPARE(spySuccess.count(), 0); } void encryptedConnectionSucceeds_data() { QTest::addColumn("psk"); QTest::addColumn("sslConfigServer"); QTest::addColumn >("trustedCertificates"); auto& settings = Env::getSingleton()->getRemoteServiceSettings(); QVERIFY(settings.checkAndGenerateKey()); const KeyPair pair = KeyPair::generate(); QVERIFY(pair.isValid()); QSslConfiguration config = SecureStorage::getInstance().getTlsConfigRemote().getConfiguration(); config.setPrivateKey(pair.getKey()); config.setLocalCertificate(pair.getCertificate()); config.setCaCertificates({KeyPair::generate().getCertificate(), settings.getCertificate(), KeyPair::generate().getCertificate()}); QTest::newRow("paired") << QString() << config << QList({KeyPair::generate().getCertificate(), pair.getCertificate(), KeyPair::generate().getCertificate()}); config = SecureStorage::getInstance().getTlsConfigRemote(SecureStorage::TlsSuite::PSK).getConfiguration(); config.setPrivateKey(pair.getKey()); config.setLocalCertificate(pair.getCertificate()); QTest::newRow("unpaired") << QString("123456") << config << QList(); } void encryptedConnectionSucceeds() { QFETCH(QString, psk); QFETCH(QSslConfiguration, sslConfigServer); QFETCH(QList, trustedCertificates); QWebSocketServer webSocketServer(QStringLiteral("Smartphone1"), QWebSocketServer::SecureMode); connect(&webSocketServer, &QWebSocketServer::preSharedKeyAuthenticationRequired, this, [&](QSslPreSharedKeyAuthenticator* pAuthenticator){ pAuthenticator->setPreSharedKey(psk.toLatin1()); }); QSignalSpy spySocketError(&webSocketServer, &QWebSocketServer::serverError); QSignalSpy spySocketSuccess(&webSocketServer, &QWebSocketServer::newConnection); auto& settings = Env::getSingleton()->getRemoteServiceSettings(); settings.setTrustedCertificates(trustedCertificates); webSocketServer.setSslConfiguration(sslConfigServer); #ifndef QT_NO_NETWORKPROXY // Listening with proxy leads to socket error QNativeSocketEnginePrivate::InvalidProxyTypeString webSocketServer.setProxy(QNetworkProxy(QNetworkProxy::NoProxy)); #endif webSocketServer.listen(QHostAddress(QHostAddress::LocalHost)); const quint16 serverPort = webSocketServer.serverPort(); // Set up client thread. QThread clientThread; // Execute test in internal block so that all relevant smart pointers are released before stopping the client thread. QSharedPointer remoteDispatcherDestructionSpy; { const QSharedPointer connector(new RemoteConnectorImpl()); QSignalSpy spyConnectorError(connector.data(), &RemoteConnector::fireRemoteDispatcherError); QSignalSpy spyConnectorSuccess(connector.data(), &RemoteConnector::fireRemoteDispatcherCreated); connector->moveToThread(&clientThread); clientThread.start(); // Send valid encrypted connect request. const QHostAddress hostAddress(QHostAddress::LocalHost); const Discovery discoveryMsg(QStringLiteral("Smartphone1"), QStringLiteral("0123456789ABCDEF"), serverPort, {IfdVersion::Version::v0}); sendRequest(connector, hostAddress, discoveryMsg, psk); QTRY_COMPARE(spyConnectorSuccess.count(), 1); QCOMPARE(spyConnectorError.count(), 0); verifySuccessSignal(spyConnectorSuccess, serverPort); const QVariant dispatcherVariant = spyConnectorSuccess.first().at(1); QVERIFY(dispatcherVariant.canConvert >()); const QSharedPointer dispatcher = dispatcherVariant.value >(); remoteDispatcherDestructionSpy.reset(new QSignalSpy(dispatcher.data(), &QObject::destroyed)); } QCOMPARE(spySocketError.count(), 0); QCOMPARE(spySocketSuccess.count(), 1); QTRY_COMPARE(remoteDispatcherDestructionSpy->count(), 1); QCOMPARE(remoteDispatcherDestructionSpy->count(), 1); clientThread.exit(); QVERIFY(clientThread.wait()); // Make sure that no pending web socket events are in the event queue when the test completes. // Pending events were occasionally delivered after the web socket had been destroyed, leading to a crash. QCoreApplication::processEvents(); } void encryptedConnectionWithWrongPasswordFails() { QWebSocketServer webSocketServer(QStringLiteral("Smartphone1"), QWebSocketServer::SecureMode); QObject::connect(&webSocketServer, &QWebSocketServer::preSharedKeyAuthenticationRequired, [](QSslPreSharedKeyAuthenticator* pAuthenticator){ pAuthenticator->setPreSharedKey(QByteArray("secret")); }); QSignalSpy spySocketError(&webSocketServer, &QWebSocketServer::serverError); QSignalSpy spySocketSuccess(&webSocketServer, &QWebSocketServer::newConnection); QSslConfiguration config = SecureStorage::getInstance().getTlsConfigRemote().getConfiguration(); const KeyPair pair = KeyPair::generate(); config.setPrivateKey(pair.getKey()); config.setLocalCertificate(pair.getCertificate()); webSocketServer.setSslConfiguration(config); #ifndef QT_NO_NETWORKPROXY // Listening with proxy leads to socket error QNativeSocketEnginePrivate::InvalidProxyTypeString webSocketServer.setProxy(QNetworkProxy(QNetworkProxy::NoProxy)); #endif webSocketServer.listen(QHostAddress(QHostAddress::LocalHost)); const quint16 serverPort = webSocketServer.serverPort(); // Set up client thread. QThread clientThread; const QSharedPointer connector(new RemoteConnectorImpl()); QSignalSpy spyConnectorError(connector.data(), &RemoteConnector::fireRemoteDispatcherError); QSignalSpy spyConnectorSuccess(connector.data(), &RemoteConnector::fireRemoteDispatcherCreated); connector->moveToThread(&clientThread); clientThread.start(); // Send encrypted connect request with wrong psk. const QHostAddress hostAddress(QHostAddress::LocalHost); const Discovery discoveryMsg(QStringLiteral("Smartphone1"), QStringLiteral("0123456789ABCDEF"), serverPort, {IfdVersion::Version::v0}); sendRequest(connector, hostAddress, discoveryMsg, QStringLiteral("sekret")); QTRY_COMPARE(spyConnectorError.count(), 1); QCOMPARE(spyConnectorSuccess.count(), 0); verifyErrorSignal(spyConnectorError, {RemoteErrorCode::REMOTE_HOST_REFUSED_CONNECTION}, serverPort, QStringLiteral("Smartphone1")); QCOMPARE(spySocketError.count(), 0); QCOMPARE(spySocketSuccess.count(), 0); clientThread.exit(); QVERIFY(clientThread.wait()); } }; QTEST_GUILESS_MAIN(test_RemoteConnector) #include "test_RemoteConnector.moc"