diff --git a/client/client.pro b/client/client.pro index c50791f..4af05f0 100644 --- a/client/client.pro +++ b/client/client.pro @@ -11,7 +11,7 @@ LIBS += -L$$top_builddir/libnymea-remoteproxyclient/ -lnymea-remoteproxyclient SOURCES += main.cpp \ proxyclient.cpp -target.path = /usr/bin +target.path = $$[QT_INSTALL_PREFIX]/bin INSTALLS += target HEADERS += \ diff --git a/client/main.cpp b/client/main.cpp index adfc87f..c7586a6 100644 --- a/client/main.cpp +++ b/client/main.cpp @@ -98,7 +98,7 @@ int main(int argc, char *argv[]) "a server application as client perspective.\n\n" "Version: %1\n" "API version: %2\n\n" - "Copyright %3 2018 Simon Stürz \n") + "Copyright %3 2021 nymea GmbH \n") .arg(SERVER_VERSION_STRING) .arg(API_VERSION_STRING) .arg(QChar(0xA9))); diff --git a/create-coverage-html.sh b/create-coverage-html.sh deleted file mode 100755 index cbbc6d1..0000000 --- a/create-coverage-html.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Export the library path for now -export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(pwd)/libnymea-remoteproxy:$(pwd)/libnymea-remoteproxyclient - -# Build -qmake CONFIG+=coverage CONFIG+=ccache -make -j$(nproc) -#make check -make coverage-html - -# Clean build -make clean - -# Clean source directory -rm -v Makefile - -rm -v libnymea-remoteproxy/libnymea-remoteproxy.so* -rm -v libnymea-remoteproxy/Makefile - -rm -v libnymea-remoteproxyclient/libnymea-remoteproxyclient.so* -rm -v libnymea-remoteproxyclient/Makefile - -rm -v server/nymea-remoteproxy -rm -v server/Makefile - -rm -v tests/Makefile - -rm -v tests/test-offline/nymea-remoteproxy-tests-offline -rm -v tests/test-offline/Makefile - -#rm -v tests/test-online/nymea-remoteproxy-tests-online -#rm -v tests/test-online/Makefile - -rm -v client/nymea-remoteproxy-client -rm -v client/Makefile diff --git a/debian/nymea-remoteproxy.install.in b/debian/nymea-remoteproxy.install.in index 888197e..3522567 100644 --- a/debian/nymea-remoteproxy.install.in +++ b/debian/nymea-remoteproxy.install.in @@ -1,2 +1,3 @@ usr/bin/nymea-remoteproxy nymea-remoteproxy.conf etc/nymea/ +nymea-remoteproxy-logging.conf etc/nymea/ diff --git a/debian/nymea-remoteproxy.service b/debian/nymea-remoteproxy.service index af023ee..ccb8118 100644 --- a/debian/nymea-remoteproxy.service +++ b/debian/nymea-remoteproxy.service @@ -6,6 +6,7 @@ Wants=network-online.target [Service] Type=simple +Environment=QT_LOGGING_CONF=/etc/nymea/nymea-remoteproxy-logging.conf ExecStart=/usr/bin/nymea-remoteproxy -c /etc/nymea/nymea-remoteproxy.conf StandardOutput=journal StandardError=journal diff --git a/debian/rules b/debian/rules index 217cc12..db4027c 100755 --- a/debian/rules +++ b/debian/rules @@ -15,6 +15,9 @@ override_dh_strip: dh_strip --dbg-package=libnymea-remoteproxy-dbg dh_strip --dbg-package=libnymea-remoteproxyclient-dbg +#override_dh_auto_test: +# make test + override_dh_auto_clean: dh_auto_clean find . -name *.qm -exec rm {} \; diff --git a/generate-coverage.sh b/generate-coverage.sh new file mode 100755 index 0000000..9fd4ac4 --- /dev/null +++ b/generate-coverage.sh @@ -0,0 +1,12 @@ +#!/bin/sh +SCRIPT_DIR=$(dirname $0) +SRC_DIR="$SCRIPT_DIR/../build-nymea-remoteproxy-Desktop-Debug/" +COV_DIR="$SRC_DIR/coverage" +HTML_RESULTS="${COV_DIR}/html" + +# Build code coverage html report +mkdir -p ${HTML_RESULTS} +lcov -d "${SRC_DIR}" -c -o "${COV_DIR}/coverage.info" +lcov -r "${COV_DIR}/coverage.info" "*.h" "*/tests/*" "*.moc" "*moc_*.cpp" "*/test/*" "/usr/include/*" "*/build*/*" "*libnymea-remoteproxy/authentication/aws*" -o "${COV_DIR}/coverage-filtered.info" +genhtml -o "${HTML_RESULTS}" "${COV_DIR}/coverage-filtered.info" +lcov -d "${COV_DIR}" -z diff --git a/libnymea-remoteproxy/authentication/authenticationreply.cpp b/libnymea-remoteproxy/authentication/authenticationreply.cpp index c7fa483..bbbb7af 100644 --- a/libnymea-remoteproxy/authentication/authenticationreply.cpp +++ b/libnymea-remoteproxy/authentication/authenticationreply.cpp @@ -26,23 +26,30 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "engine.h" +#include "loggingcategories.h" #include "authenticationreply.h" #include "authentication/authenticator.h" namespace remoteproxy { AuthenticationReply::AuthenticationReply(ProxyClient *proxyClient, QObject *parent) : - QObject(parent), - m_proxyClient(proxyClient) + QObject(parent) { + m_proxyClient = proxyClient; + m_timer = new QTimer(this); m_timer->setSingleShot(true); connect(m_timer, &QTimer::timeout, this, &AuthenticationReply::onTimeout); - + qCDebug(dcAuthentication) << "Created authentication reply for" << proxyClient << "Timeout:" << Engine::instance()->configuration()->authenticationTimeout() << "[ms]"; m_timer->start(Engine::instance()->configuration()->authenticationTimeout()); } -ProxyClient *AuthenticationReply::proxyClient() const +AuthenticationReply::~AuthenticationReply() +{ + +} + +QPointer AuthenticationReply::proxyClient() const { return m_proxyClient; } diff --git a/libnymea-remoteproxy/authentication/authenticationreply.h b/libnymea-remoteproxy/authentication/authenticationreply.h index e57bf69..cf4d82f 100644 --- a/libnymea-remoteproxy/authentication/authenticationreply.h +++ b/libnymea-remoteproxy/authentication/authenticationreply.h @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -44,7 +45,7 @@ class AuthenticationReply : public QObject public: friend class Authenticator; - ProxyClient *proxyClient() const; + QPointer proxyClient() const; bool isTimedOut() const; bool isFinished() const; @@ -53,7 +54,9 @@ public: private: explicit AuthenticationReply(ProxyClient *proxyClient, QObject *parent = nullptr); - ProxyClient *m_proxyClient = nullptr; + ~AuthenticationReply(); + + QPointer m_proxyClient; QTimer *m_timer = nullptr; bool m_timedOut = false; diff --git a/libnymea-remoteproxy/authentication/authenticator.cpp b/libnymea-remoteproxy/authentication/authenticator.cpp index d6c1b7f..329d76e 100644 --- a/libnymea-remoteproxy/authentication/authenticator.cpp +++ b/libnymea-remoteproxy/authentication/authenticator.cpp @@ -37,6 +37,11 @@ Authenticator::Authenticator(QObject *parent) : } +Authenticator::~Authenticator() +{ + +} + void Authenticator::setReplyError(AuthenticationReply *reply, Authenticator::AuthenticationError error) { reply->setError(error); @@ -52,9 +57,4 @@ AuthenticationReply *Authenticator::createAuthenticationReply(ProxyClient *proxy return new AuthenticationReply(proxyClient, parent); } -Authenticator::~Authenticator() -{ - -} - } diff --git a/libnymea-remoteproxy/engine.cpp b/libnymea-remoteproxy/engine.cpp index 887cd83..82bee33 100644 --- a/libnymea-remoteproxy/engine.cpp +++ b/libnymea-remoteproxy/engine.cpp @@ -72,20 +72,32 @@ void Engine::start(ProxyConfiguration *configuration) Q_ASSERT_X(m_authenticator != nullptr, "Engine", "There is no authenticator registerd."); m_proxyServer = new ProxyServer(this); - m_webSocketServer = new WebSocketServer(m_configuration->sslConfiguration(), this); + m_webSocketServer = new WebSocketServer(m_configuration->sslEnabled(), m_configuration->sslConfiguration(), this); + m_tcpSocketServer = new TcpSocketServer(m_configuration->sslEnabled(), m_configuration->sslConfiguration(), this); + // Configure websocket server QUrl websocketServerUrl; - websocketServerUrl.setScheme("wss"); + websocketServerUrl.setScheme(m_configuration->sslEnabled() ? "wss" : "ws"); websocketServerUrl.setHost(m_configuration->webSocketServerHost().toString()); websocketServerUrl.setPort(m_configuration->webSocketServerPort()); - m_webSocketServer->setServerUrl(websocketServerUrl); - m_proxyServer->registerTransportInterface(m_webSocketServer); + // Configure tcp socket server + QUrl tcpSocketServerUrl; + tcpSocketServerUrl.setScheme(m_configuration->sslEnabled() ? "ssl" : "tcp"); + tcpSocketServerUrl.setHost(m_configuration->tcpServerHost().toString()); + tcpSocketServerUrl.setPort(m_configuration->tcpServerPort()); + m_tcpSocketServer->setServerUrl(tcpSocketServerUrl); + // Register the transport interfaces in the proxy server + m_proxyServer->registerTransportInterface(m_webSocketServer); + m_proxyServer->registerTransportInterface(m_tcpSocketServer); + + // Start the server qCDebug(dcEngine()) << "Starting proxy server"; m_proxyServer->startServer(); + // Start the monitor server m_monitorServer = new MonitorServer(configuration->monitorSocketFileName(), this); m_monitorServer->startServer(); @@ -153,6 +165,11 @@ ProxyServer *Engine::proxyServer() const return m_proxyServer; } +TcpSocketServer *Engine::tcpSocketServer() const +{ + return m_tcpSocketServer; +} + WebSocketServer *Engine::webSocketServer() const { return m_webSocketServer; diff --git a/libnymea-remoteproxy/engine.h b/libnymea-remoteproxy/engine.h index 247a268..849ae6f 100644 --- a/libnymea-remoteproxy/engine.h +++ b/libnymea-remoteproxy/engine.h @@ -38,6 +38,7 @@ #include "logengine.h" #include "proxyserver.h" #include "monitorserver.h" +#include "tcpsocketserver.h" #include "websocketserver.h" #include "proxyconfiguration.h" #include "authentication/authenticator.h" @@ -67,6 +68,7 @@ public: ProxyConfiguration *configuration() const; Authenticator *authenticator() const; ProxyServer *proxyServer() const; + TcpSocketServer *tcpSocketServer() const; WebSocketServer *webSocketServer() const; MonitorServer *monitorServer() const; LogEngine *logEngine() const; @@ -88,6 +90,7 @@ private: ProxyConfiguration *m_configuration = nullptr; Authenticator *m_authenticator = nullptr; ProxyServer *m_proxyServer = nullptr; + TcpSocketServer *m_tcpSocketServer = nullptr; WebSocketServer *m_webSocketServer = nullptr; MonitorServer *m_monitorServer = nullptr; LogEngine *m_logEngine = nullptr; diff --git a/libnymea-remoteproxy/jsonrpc/authenticationhandler.cpp b/libnymea-remoteproxy/jsonrpc/authenticationhandler.cpp index 127058f..57ab1fb 100644 --- a/libnymea-remoteproxy/jsonrpc/authenticationhandler.cpp +++ b/libnymea-remoteproxy/jsonrpc/authenticationhandler.cpp @@ -89,7 +89,7 @@ void AuthenticationHandler::onAuthenticationFinished() AuthenticationReply *authenticationReply = static_cast(sender()); authenticationReply->deleteLater(); - qCDebug(dcJsonRpc()) << "Authentication response ready for" << authenticationReply->proxyClient() << authenticationReply->error(); + qCDebug(dcJsonRpc()) << "Authentication reply finished"; JsonReply *jsonReply = m_runningAuthentications.take(authenticationReply); if (authenticationReply->error() != Authenticator::AuthenticationErrorNoError) { @@ -100,10 +100,12 @@ void AuthenticationHandler::onAuthenticationFinished() jsonReply->setSuccess(true); } - // Set client authenticated - authenticationReply->proxyClient()->setAuthenticated(authenticationReply->error() == Authenticator::AuthenticationErrorNoError); + // Set client authenticated if still there + if (!authenticationReply->proxyClient().isNull()) { + authenticationReply->proxyClient()->setAuthenticated(authenticationReply->error() == Authenticator::AuthenticationErrorNoError); + jsonReply->setData(errorToReply(authenticationReply->error())); + } - jsonReply->setData(errorToReply(authenticationReply->error())); jsonReply->finished(); } diff --git a/libnymea-remoteproxy/jsonrpcserver.cpp b/libnymea-remoteproxy/jsonrpcserver.cpp index 8b64e0f..6753967 100644 --- a/libnymea-remoteproxy/jsonrpcserver.cpp +++ b/libnymea-remoteproxy/jsonrpcserver.cpp @@ -173,80 +173,10 @@ void JsonRpcServer::unregisterHandler(JsonHandler *handler) m_handlers.remove(handler->name()); } -void JsonRpcServer::setup() +void JsonRpcServer::processDataPackage(ProxyClient *proxyClient, const QByteArray &data) { - registerHandler(this); - registerHandler(new AuthenticationHandler(this)); -} - -void JsonRpcServer::asyncReplyFinished() -{ - JsonReply *reply = static_cast(sender()); - reply->deleteLater(); - - ProxyClient *proxyClient = m_asyncReplies.take(reply); - qCDebug(dcJsonRpc()) << "Async reply finished" << reply->handler()->name() << reply->method() << reply->clientId().toString(); - - if (!proxyClient) { - qCWarning(dcJsonRpc()) << "Got an async reply but the client does not exist any more"; - return; - } - - if (!reply->timedOut()) { - Q_ASSERT_X(reply->handler()->validateReturns(reply->method(), reply->data()).first - ,"validating return value", formatAssertion(reply->handler()->name(), - reply->method(), reply->handler(), reply->data()).toLatin1().data()); - - QPair returnValidation = reply->handler()->validateReturns(reply->method(), reply->data()); - if (!returnValidation.first) { - qCWarning(dcJsonRpc()) << "Return value validation failed. This should never happen. Please check the source code."; - } - - sendResponse(proxyClient, reply->commandId(), reply->data()); - - if (!reply->success()) { - // Disconnect this client since the request was not successfully - proxyClient->interface()->killClientConnection(proxyClient->clientId(), "API call was not successfully."); - } - - } else { - sendErrorResponse(proxyClient, reply->commandId(), "Command timed out"); - // Disconnect this client since he requested something that created a timeout - proxyClient->killConnection("API call timeouted."); - } -} - -void JsonRpcServer::registerClient(ProxyClient *proxyClient) -{ - qCDebug(dcJsonRpc()) << "Register client" << proxyClient; - if (m_clients.contains(proxyClient)) { - qCWarning(dcJsonRpc()) << "Client already registered" << proxyClient; - return; - } - m_clients.append(proxyClient); -} - -void JsonRpcServer::unregisterClient(ProxyClient *proxyClient) -{ - qCDebug(dcJsonRpc()) << "Unregister client" << proxyClient; - if (!m_clients.contains(proxyClient)) { - qCWarning(dcJsonRpc()) << "Client was not registered" << proxyClient; - return; - } - - m_clients.removeAll(proxyClient); -} - -void JsonRpcServer::processData(ProxyClient *proxyClient, const QByteArray &data) -{ - if (!m_clients.contains(proxyClient)) - return; - - qCDebug(dcJsonRpcTraffic()) << "Incoming data from" << proxyClient << ": " << data; - QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); - if(error.error != QJsonParseError::NoError) { qCWarning(dcJsonRpc) << "Failed to parse JSON data" << data << ":" << error.errorString(); sendErrorResponse(proxyClient, -1, QString("Failed to parse JSON data: %1").arg(error.errorString())); @@ -322,6 +252,99 @@ void JsonRpcServer::processData(ProxyClient *proxyClient, const QByteArray &data } } +void JsonRpcServer::setup() +{ + registerHandler(this); + registerHandler(new AuthenticationHandler(this)); +} + +void JsonRpcServer::asyncReplyFinished() +{ + JsonReply *reply = static_cast(sender()); + reply->deleteLater(); + + ProxyClient *proxyClient = m_asyncReplies.take(reply); + qCDebug(dcJsonRpc()) << "Async reply finished" << reply->handler()->name() << reply->method() << reply->clientId().toString(); + + if (!proxyClient) { + qCWarning(dcJsonRpc()) << "Got an async reply but the client does not exist any more."; + return; + } + + if (!reply->timedOut()) { + Q_ASSERT_X(reply->handler()->validateReturns(reply->method(), reply->data()).first + ,"validating return value", formatAssertion(reply->handler()->name(), + reply->method(), reply->handler(), reply->data()).toLatin1().data()); + + QPair returnValidation = reply->handler()->validateReturns(reply->method(), reply->data()); + if (!returnValidation.first) { + qCWarning(dcJsonRpc()) << "Return value validation failed. This should never happen. Please check the source code."; + } + + sendResponse(proxyClient, reply->commandId(), reply->data()); + + if (!reply->success()) { + // Disconnect this client since the request was not successfully + proxyClient->interface()->killClientConnection(proxyClient->clientId(), "API call was not successfully."); + } + + } else { + qCWarning(dcJsonRpc()) << "The reply timeouted."; + sendErrorResponse(proxyClient, reply->commandId(), "Command timed out"); + // Disconnect this client since he requested something that created a timeout + proxyClient->killConnection("API call timeouted."); + } +} + +void JsonRpcServer::registerClient(ProxyClient *proxyClient) +{ + qCDebug(dcJsonRpc()) << "Register client" << proxyClient; + if (m_clients.contains(proxyClient)) { + qCWarning(dcJsonRpc()) << "Client already registered" << proxyClient; + return; + } + m_clients.append(proxyClient); +} + +void JsonRpcServer::unregisterClient(ProxyClient *proxyClient) +{ + qCDebug(dcJsonRpc()) << "Unregister client" << proxyClient; + if (!m_clients.contains(proxyClient)) { + qCWarning(dcJsonRpc()) << "Client was not registered" << proxyClient; + return; + } + m_clients.removeAll(proxyClient); + + if (m_asyncReplies.values().contains(proxyClient)) { + qCWarning(dcJsonRpc()) << "Client was still waiting for a reply. Clean up reply"; + JsonReply *reply = m_asyncReplies.key(proxyClient); + m_asyncReplies.remove(reply); + // Note: the reply will be deleted in the finished slot + } +} + +void JsonRpcServer::processData(ProxyClient *proxyClient, const QByteArray &data) +{ + if (!m_clients.contains(proxyClient)) + return; + + qCDebug(dcJsonRpcTraffic()) << "Incoming data from" << proxyClient << ": " << data; + + // Handle package fragmentation + QList packages = proxyClient->processData(data); + + // Make sure the buffer size is in range + if (proxyClient->bufferSize() > 1024 * 10) { + qCWarning(dcJsonRpc()) << "Data buffer size violation from" << proxyClient; + proxyClient->killConnection("Data buffer size violation."); + return; + } + + foreach (const QByteArray &package, packages) { + processDataPackage(proxyClient, package); + } +} + void JsonRpcServer::sendNotification(const QString &nameSpace, const QString &method, const QVariantMap ¶ms, ProxyClient *proxyClient) { QVariantMap notification; diff --git a/libnymea-remoteproxy/jsonrpcserver.h b/libnymea-remoteproxy/jsonrpcserver.h index 45f996a..19249da 100644 --- a/libnymea-remoteproxy/jsonrpcserver.h +++ b/libnymea-remoteproxy/jsonrpcserver.h @@ -58,6 +58,7 @@ private: QHash m_handlers; QHash m_asyncReplies; QList m_clients; + int m_notificationId = 0; void sendResponse(ProxyClient *client, int commandId, const QVariantMap ¶ms = QVariantMap()); @@ -67,6 +68,7 @@ private: void registerHandler(JsonHandler *handler); void unregisterHandler(JsonHandler *handler); + void processDataPackage(ProxyClient *proxyClient, const QByteArray &data); private slots: void setup(); diff --git a/libnymea-remoteproxy/libnymea-remoteproxy.pro b/libnymea-remoteproxy/libnymea-remoteproxy.pro index 1edbfa2..373a8e8 100644 --- a/libnymea-remoteproxy/libnymea-remoteproxy.pro +++ b/libnymea-remoteproxy/libnymea-remoteproxy.pro @@ -6,6 +6,7 @@ TARGET = nymea-remoteproxy HEADERS += \ engine.h \ loggingcategories.h \ + tcpsocketserver.h \ transportinterface.h \ websocketserver.h \ proxyclient.h \ @@ -31,6 +32,7 @@ HEADERS += \ SOURCES += \ engine.cpp \ loggingcategories.cpp \ + tcpsocketserver.cpp \ transportinterface.cpp \ websocketserver.cpp \ proxyclient.cpp \ diff --git a/libnymea-remoteproxy/loggingcategories.cpp b/libnymea-remoteproxy/loggingcategories.cpp index b4aaae6..5bf36b7 100644 --- a/libnymea-remoteproxy/loggingcategories.cpp +++ b/libnymea-remoteproxy/loggingcategories.cpp @@ -30,7 +30,10 @@ Q_LOGGING_CATEGORY(dcApplication, "Application") Q_LOGGING_CATEGORY(dcEngine, "Engine") Q_LOGGING_CATEGORY(dcJsonRpc, "JsonRpc") +Q_LOGGING_CATEGORY(dcTunnel, "Tunnel") Q_LOGGING_CATEGORY(dcJsonRpcTraffic, "JsonRpcTraffic") +Q_LOGGING_CATEGORY(dcTcpSocketServer, "TcpSocketServer") +Q_LOGGING_CATEGORY(dcTcpSocketServerTraffic, "TcpSocketServerTraffic") Q_LOGGING_CATEGORY(dcWebSocketServer, "WebSocketServer") Q_LOGGING_CATEGORY(dcWebSocketServerTraffic, "WebSocketServerTraffic") Q_LOGGING_CATEGORY(dcAuthentication, "Authentication") diff --git a/libnymea-remoteproxy/loggingcategories.h b/libnymea-remoteproxy/loggingcategories.h index 0d79c02..f9b4464 100644 --- a/libnymea-remoteproxy/loggingcategories.h +++ b/libnymea-remoteproxy/loggingcategories.h @@ -34,9 +34,12 @@ Q_DECLARE_LOGGING_CATEGORY(dcApplication) Q_DECLARE_LOGGING_CATEGORY(dcEngine) Q_DECLARE_LOGGING_CATEGORY(dcJsonRpc) +Q_DECLARE_LOGGING_CATEGORY(dcTunnel) Q_DECLARE_LOGGING_CATEGORY(dcJsonRpcTraffic) Q_DECLARE_LOGGING_CATEGORY(dcWebSocketServer) Q_DECLARE_LOGGING_CATEGORY(dcWebSocketServerTraffic) +Q_DECLARE_LOGGING_CATEGORY(dcTcpSocketServer) +Q_DECLARE_LOGGING_CATEGORY(dcTcpSocketServerTraffic) Q_DECLARE_LOGGING_CATEGORY(dcAuthentication) Q_DECLARE_LOGGING_CATEGORY(dcAuthenticationProcess) Q_DECLARE_LOGGING_CATEGORY(dcProxyServer) diff --git a/libnymea-remoteproxy/proxyclient.cpp b/libnymea-remoteproxy/proxyclient.cpp index 7a95e90..0fc0bfc 100644 --- a/libnymea-remoteproxy/proxyclient.cpp +++ b/libnymea-remoteproxy/proxyclient.cpp @@ -27,6 +27,7 @@ #include "engine.h" #include "proxyclient.h" +#include "loggingcategories.h" #include @@ -40,9 +41,10 @@ ProxyClient::ProxyClient(TransportInterface *interface, const QUuid &clientId, c { m_creationTimeStamp = QDateTime::currentDateTime().toTime_t(); - connect(&m_timer, &QTimer::timeout, this, &ProxyClient::timeoutOccured); - m_timer.setSingleShot(true); - m_timer.start(Engine::instance()->configuration()->inactiveTimeout()); + m_timer = new QTimer(this); + connect(m_timer, &QTimer::timeout, this, &ProxyClient::timeoutOccured); + m_timer->setSingleShot(true); + resetTimer(); } QUuid ProxyClient::clientId() const @@ -74,8 +76,8 @@ void ProxyClient::setAuthenticated(bool isAuthenticated) { m_authenticated = isAuthenticated; if (m_authenticated) { - m_timer.stop(); - m_timer.start(Engine::instance()->configuration()->aloneTimeout()); + m_timerWaitState = TimerWaitStateAlone; + resetTimer(); emit authenticated(); } } @@ -89,7 +91,7 @@ void ProxyClient::setTunnelConnected(bool isTunnelConnected) { m_tunnelConnected = isTunnelConnected; if (m_tunnelConnected) { - m_timer.stop(); + m_timer->stop(); emit tunnelConnected(); } } @@ -174,6 +176,25 @@ void ProxyClient::addTxDataCount(int dataCount) m_txDataCount += static_cast(dataCount); } +ProxyClient::TimerWaitState ProxyClient::timerWaitState() const +{ + return m_timerWaitState; +} + +void ProxyClient::resetTimer() +{ + switch (m_timerWaitState) { + case TimerWaitStateInactive: + m_timer->stop(); + m_timer->start(Engine::instance()->configuration()->inactiveTimeout()); + break; + case TimerWaitStateAlone: + m_timer->stop(); + m_timer->start(Engine::instance()->configuration()->aloneTimeout()); + break; + } +} + void ProxyClient::sendData(const QByteArray &data) { if (!m_interface) @@ -190,6 +211,37 @@ void ProxyClient::killConnection(const QString &reason) m_interface->killClientConnection(m_clientId, reason); } +int ProxyClient::generateMessageId() +{ + m_messageId++; + return m_messageId; +} + +QList ProxyClient::processData(const QByteArray &data) +{ + QList packages; + + // Handle packet fragmentation + m_dataBuffers.append(data); + int splitIndex = m_dataBuffers.indexOf("}\n{"); + while (splitIndex > -1) { + packages.append(m_dataBuffers.left(splitIndex + 1)); + m_dataBuffers = m_dataBuffers.right(m_dataBuffers.length() - splitIndex - 2); + splitIndex = m_dataBuffers.indexOf("}\n{"); + } + if (m_dataBuffers.trimmed().endsWith("}")) { + packages.append(m_dataBuffers); + m_dataBuffers.clear(); + } + + return packages; +} + +int ProxyClient::bufferSize() const +{ + return m_dataBuffers.size(); +} + QDebug operator<<(QDebug debug, ProxyClient *proxyClient) { debug.nospace() << "ProxyClient("; @@ -200,8 +252,8 @@ QDebug operator<<(QDebug debug, ProxyClient *proxyClient) debug.nospace() << ", " << proxyClient->clientId().toString(); debug.nospace() << ", " << proxyClient->userName(); debug.nospace() << ", " << proxyClient->peerAddress().toString(); - debug.nospace() << ", " << proxyClient->creationTimeString() << ") "; - return debug; + debug.nospace() << ", " << proxyClient->creationTimeString() << ")"; + return debug.space(); } } diff --git a/libnymea-remoteproxy/proxyclient.h b/libnymea-remoteproxy/proxyclient.h index 58b83ac..492a429 100644 --- a/libnymea-remoteproxy/proxyclient.h +++ b/libnymea-remoteproxy/proxyclient.h @@ -43,6 +43,12 @@ class ProxyClient : public QObject Q_OBJECT public: + enum TimerWaitState { + TimerWaitStateInactive, + TimerWaitStateAlone + }; + Q_ENUM(TimerWaitState) + explicit ProxyClient(TransportInterface *interface, const QUuid &clientId, const QHostAddress &address, QObject *parent = nullptr); QUuid clientId() const; @@ -84,12 +90,20 @@ public: void addTxDataCount(int dataCount); // Actions for this client + TimerWaitState timerWaitState() const; + void resetTimer(); void sendData(const QByteArray &data); void killConnection(const QString &reason); + // Json server methods + int generateMessageId(); + QList processData(const QByteArray &data); + int bufferSize() const; + private: TransportInterface *m_interface = nullptr; - QTimer m_timer; + QTimer *m_timer = nullptr; + TimerWaitState m_timerWaitState = TimerWaitStateInactive; QUuid m_clientId; QHostAddress m_peerAddress; @@ -105,6 +119,11 @@ private: QString m_userName; + // Json data information + int m_messageId = 0; + QByteArray m_dataBuffers; + bool m_bufferSizeViolation = false; + quint64 m_rxDataCount = 0; quint64 m_txDataCount = 0; diff --git a/libnymea-remoteproxy/proxyconfiguration.cpp b/libnymea-remoteproxy/proxyconfiguration.cpp index 7d2f061..5d88dc0 100644 --- a/libnymea-remoteproxy/proxyconfiguration.cpp +++ b/libnymea-remoteproxy/proxyconfiguration.cpp @@ -71,6 +71,7 @@ bool ProxyConfiguration::loadConfiguration(const QString &fileName) settings.endGroup(); settings.beginGroup("SSL"); + setSslEnabled(settings.value("enabled", true).toBool()); setSslCertificateFileName(settings.value("certificate", "/etc/ssl/certs/ssl-cert-snakeoil.pem").toString()); setSslCertificateKeyFileName(settings.value("certificateKey", "/etc/ssl/private/ssl-cert-snakeoil.key").toString()); setSslCertificateChainFileName(settings.value("certificateChain", "").toString()); @@ -256,6 +257,16 @@ void ProxyConfiguration::setAwsCredentialsUrl(const QUrl &url) m_awsCredentialsUrl = url; } +bool ProxyConfiguration::sslEnabled() const +{ + return m_sslEnabled; +} + +void ProxyConfiguration::setSslEnabled(bool enabled) +{ + m_sslEnabled = enabled; +} + QString ProxyConfiguration::sslCertificateFileName() const { return m_sslCertificateFileName; @@ -349,6 +360,7 @@ QDebug operator<<(QDebug debug, ProxyConfiguration *configuration) debug.nospace() << " - Authorizer lambda function:" << configuration->awsAuthorizerLambdaFunctionName() << endl; debug.nospace() << " - Credentials URL:" << configuration->awsCredentialsUrl().toString() << endl; debug.nospace() << "SSL configuration" << endl; + debug.nospace() << " - Enabled:" << configuration->sslEnabled() << endl; debug.nospace() << " - Certificate:" << configuration->sslCertificateFileName() << endl; debug.nospace() << " - Certificate key:" << configuration->sslCertificateKeyFileName() << endl; debug.nospace() << " - Certificate chain:" << configuration->sslCertificateChainFileName() << endl; diff --git a/libnymea-remoteproxy/proxyconfiguration.h b/libnymea-remoteproxy/proxyconfiguration.h index 048c74a..2262761 100644 --- a/libnymea-remoteproxy/proxyconfiguration.h +++ b/libnymea-remoteproxy/proxyconfiguration.h @@ -85,6 +85,9 @@ public: void setAwsCredentialsUrl(const QUrl &url); // Ssl + bool sslEnabled() const; + void setSslEnabled(bool enabled); + QString sslCertificateFileName() const; void setSslCertificateFileName(const QString &fileName); @@ -130,6 +133,7 @@ private: QUrl m_awsCredentialsUrl; // Ssl + bool m_sslEnabled = true; QString m_sslCertificateFileName = "/etc/ssl/certs/ssl-cert-snakeoil.pem"; QString m_sslCertificateKeyFileName = "/etc/ssl/private/ssl-cert-snakeoil.key"; QString m_sslCertificateChainFileName; diff --git a/libnymea-remoteproxy/proxyserver.cpp b/libnymea-remoteproxy/proxyserver.cpp index 089f9cc..6048114 100644 --- a/libnymea-remoteproxy/proxyserver.cpp +++ b/libnymea-remoteproxy/proxyserver.cpp @@ -185,6 +185,8 @@ void ProxyServer::establishTunnel(ProxyClient *firstClient, ProxyClient *secondC secondClient->setTunnelConnected(true); qCDebug(dcProxyServer()) << tunnel; + qCDebug(dcTunnel()) << "Tunnel established between" << firstClient->peerAddress().toString() << firstClient->clientId() + << "<-->" << secondClient->peerAddress().toString() << secondClient->clientId(); m_totalTunnelCount += 1; saveStatistics(); @@ -242,18 +244,21 @@ void ProxyServer::onClientDisconnected(const QUuid &clientId) // Check if if (m_tunnels.contains(proxyClient->tunnelIdentifier())) { - // There is a tunnel connection for this client, remove the tunnel and disconnect also the other client ProxyClient *remoteClient = getRemoteClient(proxyClient); TunnelConnection tunnelConnection = m_tunnels.take(proxyClient->tunnelIdentifier()); Engine::instance()->logEngine()->logTunnel(tunnelConnection); if (remoteClient) { + qCDebug(dcTunnel()) << "Remove tunnel between" << proxyClient->peerAddress().toString() << proxyClient->clientId() + << "<-->" << remoteClient->peerAddress().toString() << remoteClient->clientId(); remoteClient->killConnection("Tunnel client disconnected"); } } // Delete the proxy client proxyClient->deleteLater(); + } else { + qCWarning(dcProxyServer()) << "Unknown client disconnected from proxy server." << clientId.toString(); } } @@ -269,6 +274,8 @@ void ProxyServer::onClientDataAvailable(const QUuid &clientId, const QByteArray if (!proxyClient->isAuthenticated() && !proxyClient->isTunnelConnected()) { qCDebug(dcProxyServerTraffic()) << "Client data available" << proxyClient << qUtf8Printable(data); m_jsonRpcServer->processData(proxyClient, data); + // Reset the inactive timer + proxyClient->resetTimer(); return; } @@ -368,8 +375,15 @@ void ProxyServer::onProxyClientAuthenticated() void ProxyServer::onProxyClientTimeoutOccured() { ProxyClient *proxyClient = static_cast(sender()); - qCDebug(dcProxyServer()) << "Timeout occured for" << proxyClient; - proxyClient->killConnection("Proxy timeout occuret"); + qCDebug(dcProxyServer()) << "Timeout occured for" << proxyClient; + switch (proxyClient->timerWaitState()) { + case ProxyClient::TimerWaitStateInactive: + proxyClient->killConnection("Proxy timeout occuret. The socket was inactive."); + break; + case ProxyClient::TimerWaitStateAlone: + proxyClient->killConnection("Proxy timeout occuret. The tunnel partner did not show up."); + break; + } } void ProxyServer::startServer() diff --git a/libnymea-remoteproxy/tcpsocketserver.cpp b/libnymea-remoteproxy/tcpsocketserver.cpp new file mode 100644 index 0000000..7fd358d --- /dev/null +++ b/libnymea-remoteproxy/tcpsocketserver.cpp @@ -0,0 +1,183 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Simon Stürz * + * * + * This file is part of nymea-remoteproxy. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "tcpsocketserver.h" +#include "loggingcategories.h" + +namespace remoteproxy { + +TcpSocketServer::TcpSocketServer(bool sslEnabled, const QSslConfiguration &sslConfiguration, QObject *parent) : + TransportInterface(parent), + m_sslEnabled(sslEnabled), + m_sslConfiguration(sslConfiguration) +{ + m_serverName = "TCP"; +} + +TcpSocketServer::~TcpSocketServer() +{ + stopServer(); +} + +void TcpSocketServer::sendData(const QUuid &clientId, const QByteArray &data) +{ + QTcpSocket *client = nullptr; + client = m_clientList.value(clientId); + if (!client) { + qCWarning(dcTcpSocketServer()) << "Client" << clientId << "unknown to this transport"; + return; + } + + qCDebug(dcTcpSocketServerTraffic()) << "Send data to" << clientId.toString() << data + '\n'; + if (client->write(data + '\n') < 0) { + qCWarning(dcTcpSocketServer()) << "Could not write data to client socket" << clientId.toString(); + } +} + +void TcpSocketServer::killClientConnection(const QUuid &clientId, const QString &killReason) +{ + QTcpSocket *client = m_clientList.value(clientId); + if (!client) + return; + + qCWarning(dcTcpSocketServer()) << "Killing client connection" << clientId.toString() << "Reason:" << killReason; + client->close(); +} + +bool TcpSocketServer::running() const +{ + if (!m_server) + return false; + + return m_server->isListening(); +} + +void TcpSocketServer::onDataAvailable(QSslSocket *client, const QByteArray &data) +{ + //qCDebug(dcTcpSocketServerTraffic()) << "Emitting data available internal."; + QUuid clientId = m_clientList.key(qobject_cast(client)); + emit dataAvailable(clientId, data); +} + +void TcpSocketServer::onClientConnected(QSslSocket *client) +{ + QUuid clientId = QUuid::createUuid(); + qCDebug(dcTcpSocketServer()) << "New client connected:" << client << client->peerAddress().toString() << clientId.toString(); + m_clientList.insert(clientId, client); + emit clientConnected(clientId, client->peerAddress()); +} + +void TcpSocketServer::onClientDisconnected(QSslSocket *client) +{ + QUuid clientId = m_clientList.key(client); + qCDebug(dcTcpSocketServer()) << "Client disconnected:" << client << client->peerAddress().toString() << clientId.toString(); + m_clientList.take(clientId); + // Note: the SslServer is deleting the socket object + emit clientDisconnected(clientId); +} + +bool TcpSocketServer::startServer() +{ + qCDebug(dcTcpSocketServer()) << "Starting TCP server" << m_serverUrl.toString(); + m_server = new SslServer(m_sslEnabled, m_sslConfiguration); + if(!m_server->listen(QHostAddress(m_serverUrl.host()), static_cast(m_serverUrl.port()))) { + qCWarning(dcTcpSocketServer()) << "Tcp server error: can not listen on" << m_serverUrl.toString(); + delete m_server; + m_server = nullptr; + return false; + } + + connect(m_server, &SslServer::clientConnected, this, &TcpSocketServer::onClientConnected); + connect(m_server, SIGNAL(clientDisconnected(QSslSocket *)), SLOT(onClientDisconnected(QSslSocket *))); + connect(m_server, &SslServer::dataAvailable, this, &TcpSocketServer::onDataAvailable); + qCDebug(dcTcpSocketServer()) << "Server started successfully."; + return true; +} + +bool TcpSocketServer::stopServer() +{ + // Clean up client connections + foreach (const QUuid &clientId, m_clientList.keys()) { + killClientConnection(clientId, "Stop server"); + } + + if (!m_server) + return true; + + qCDebug(dcTcpSocketServer()) << "Stop server" << m_serverUrl.toString(); + m_server->close(); + delete m_server; + m_server = nullptr; + return true; +} + +SslServer::SslServer(bool sslEnabled, const QSslConfiguration &config, QObject *parent) : + QTcpServer(parent), + m_sslEnabled(sslEnabled), + m_config(config) + +{ + +} + +void SslServer::incomingConnection(qintptr socketDescriptor) +{ + QSslSocket *sslSocket = new QSslSocket(this); + qCDebug(dcTcpSocketServer()) << "Incomming connection" << sslSocket; + connect(sslSocket, &QSslSocket::readyRead, this, &SslServer::onSocketReadyRead); + connect(sslSocket, &QSslSocket::disconnected, this, &SslServer::onClientDisconnected); + connect(sslSocket, &QSslSocket::encrypted, [this, sslSocket](){ + qCDebug(dcTcpSocketServer()) << "SSL encryption established for" << sslSocket; + emit clientConnected(sslSocket); + }); + + if (!sslSocket->setSocketDescriptor(socketDescriptor)) { + qCWarning(dcTcpSocketServer()) << "Failed to set SSL socket descriptor."; + delete sslSocket; + return; + } + + if (m_sslEnabled) { + sslSocket->setSslConfiguration(m_config); + qCDebug(dcTcpSocketServer()) << "Start SSL encryption for" << sslSocket; + sslSocket->startServerEncryption(); + } else { + emit clientConnected(sslSocket); + } +} + +void SslServer::onClientDisconnected() +{ + QSslSocket *sslSocket = qobject_cast(sender()); + qCDebug(dcTcpSocketServer()) << "Client socket disconnected:" << sslSocket << sslSocket->peerAddress().toString();; + emit clientDisconnected(sslSocket); + sslSocket->deleteLater(); +} + +void SslServer::onSocketReadyRead() +{ + QSslSocket *sslSocket = qobject_cast(sender()); + QByteArray data = sslSocket->readAll(); + qCDebug(dcTcpSocketServerTraffic()) << "Data from socket" << sslSocket->peerAddress().toString() << data; + emit dataAvailable(sslSocket, data); +} + +} diff --git a/libnymea-remoteproxy/tcpsocketserver.h b/libnymea-remoteproxy/tcpsocketserver.h new file mode 100644 index 0000000..82bea88 --- /dev/null +++ b/libnymea-remoteproxy/tcpsocketserver.h @@ -0,0 +1,100 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef TCPSOCKETSERVER_H +#define TCPSOCKETSERVER_H + +#include +#include +#include +#include + +#include "transportinterface.h" + +namespace remoteproxy { + + +class SslServer: public QTcpServer +{ + Q_OBJECT +public: + explicit SslServer(bool sslEnabled, const QSslConfiguration &config, QObject *parent = nullptr); + ~SslServer() override = default; + +private: + bool m_sslEnabled = false; + QSslConfiguration m_config; + +signals: + void clientConnected(QSslSocket *socket); + void clientDisconnected(QSslSocket *socket); + void dataAvailable(QSslSocket *socket, const QByteArray &data); + +protected: + void incomingConnection(qintptr socketDescriptor) override; + +private slots: + void onClientDisconnected(); + void onSocketReadyRead(); + +}; + + +class TcpSocketServer : public TransportInterface +{ + Q_OBJECT +public: + explicit TcpSocketServer(bool sslEnabled, const QSslConfiguration &sslConfiguration, QObject *parent = nullptr); + ~TcpSocketServer() override; + + void sendData(const QUuid &clientId, const QByteArray &data) override; + void killClientConnection(const QUuid &clientId, const QString &killReason) override; + + bool running() const override; + +private: + bool m_sslEnabled; + QSslConfiguration m_sslConfiguration; + + QHash m_clientList; + + SslServer *m_server = nullptr; + +private slots: + void onDataAvailable(QSslSocket *client, const QByteArray &data); + void onClientConnected(QSslSocket *client); + void onClientDisconnected(QSslSocket *client); + +public slots: + bool startServer() override; + bool stopServer() override; + +}; + +} + +#endif // TCPSOCKETSERVER_H diff --git a/libnymea-remoteproxy/transportinterface.cpp b/libnymea-remoteproxy/transportinterface.cpp index 578c3aa..5e969b1 100644 --- a/libnymea-remoteproxy/transportinterface.cpp +++ b/libnymea-remoteproxy/transportinterface.cpp @@ -35,14 +35,24 @@ TransportInterface::TransportInterface(QObject *parent) : } -QString TransportInterface::serverName() const -{ - return m_serverName; -} - TransportInterface::~TransportInterface() { } +QString TransportInterface::serverName() const +{ + return m_serverName; +} + +QUrl TransportInterface::serverUrl() const +{ + return m_serverUrl; +} + +void TransportInterface::setServerUrl(const QUrl &serverUrl) +{ + m_serverUrl = serverUrl; +} + } diff --git a/libnymea-remoteproxy/transportinterface.h b/libnymea-remoteproxy/transportinterface.h index aa13f7c..b507fcd 100644 --- a/libnymea-remoteproxy/transportinterface.h +++ b/libnymea-remoteproxy/transportinterface.h @@ -28,6 +28,7 @@ #ifndef TRANSPORTINTERFACE_H #define TRANSPORTINTERFACE_H +#include #include #include @@ -45,12 +46,18 @@ public: virtual void sendData(const QUuid &clientId, const QByteArray &data) = 0; virtual void killClientConnection(const QUuid &clientId, const QString &killReason) = 0; + QUrl serverUrl() const; + void setServerUrl(const QUrl &serverUrl); + + virtual bool running() const = 0; + signals: void clientConnected(const QUuid &clientId, const QHostAddress &address); void clientDisconnected(const QUuid &clientId); void dataAvailable(const QUuid &clientId, const QByteArray &data); protected: + QUrl m_serverUrl; QString m_serverName; public slots: diff --git a/libnymea-remoteproxy/websocketserver.cpp b/libnymea-remoteproxy/websocketserver.cpp index 7eb214a..d7ffa66 100644 --- a/libnymea-remoteproxy/websocketserver.cpp +++ b/libnymea-remoteproxy/websocketserver.cpp @@ -32,11 +32,12 @@ namespace remoteproxy { -WebSocketServer::WebSocketServer(const QSslConfiguration &sslConfiguration, QObject *parent) : +WebSocketServer::WebSocketServer(bool sslEnabled, const QSslConfiguration &sslConfiguration, QObject *parent) : TransportInterface(parent), + m_sslEnabled(sslEnabled), m_sslConfiguration(sslConfiguration) { - m_serverName = "Websocket server"; + m_serverName = "WebSocket"; } WebSocketServer::~WebSocketServer() @@ -44,16 +45,6 @@ WebSocketServer::~WebSocketServer() stopServer(); } -QUrl WebSocketServer::serverUrl() const -{ - return m_serverUrl; -} - -void WebSocketServer::setServerUrl(const QUrl &serverUrl) -{ - m_serverUrl = serverUrl; -} - bool WebSocketServer::running() const { if (!m_server) @@ -93,12 +84,16 @@ void WebSocketServer::onClientConnected() { // Got a new client connected QWebSocket *client = m_server->nextPendingConnection(); + if (!client) { + qCWarning(dcWebSocketServer()) << "Next pending connection dissapeared. Doing nothing."; + return; + } // Check websocket version if (client->version() != QWebSocketProtocol::Version13) { qCWarning(dcWebSocketServer()) << "Client with invalid protocol version" << client->version() << ". Rejecting."; client->close(QWebSocketProtocol::CloseCodeProtocolError, QString("invalid protocol version: %1 != Supported Version 13").arg(client->version())); - delete client; + client->deleteLater(); return; } @@ -149,7 +144,10 @@ void WebSocketServer::onBinaryMessageReceived(const QByteArray &data) void WebSocketServer::onClientError(QAbstractSocket::SocketError error) { QWebSocket *client = static_cast(sender()); - qCWarning(dcWebSocketServer()) << "Client error occurred:" << error << client->errorString(); + qCWarning(dcWebSocketServer()) << "Client error occurred:" << client << client->peerAddress().toString() << error << client->errorString() << "Closing the socket."; + + // Note: on any error which can occure, make sure the socket will be closed in any case + client->close(); } void WebSocketServer::onAcceptError(QAbstractSocket::SocketError error) @@ -164,8 +162,12 @@ void WebSocketServer::onServerError(QWebSocketProtocol::CloseCode closeCode) bool WebSocketServer::startServer() { - m_server = new QWebSocketServer(QCoreApplication::applicationName(), QWebSocketServer::SecureMode, this); - m_server->setSslConfiguration(sslConfiguration()); + if (m_sslEnabled) { + m_server = new QWebSocketServer(QCoreApplication::applicationName(), QWebSocketServer::SecureMode, this); + m_server->setSslConfiguration(sslConfiguration()); + } else { + m_server = new QWebSocketServer(QCoreApplication::applicationName(), QWebSocketServer::NonSecureMode, this); + } connect (m_server, &QWebSocketServer::newConnection, this, &WebSocketServer::onClientConnected); connect (m_server, &QWebSocketServer::acceptError, this, &WebSocketServer::onAcceptError); @@ -188,16 +190,18 @@ bool WebSocketServer::stopServer() // Clean up client connections foreach (QWebSocket *client, m_clientList.values()) { client->close(QWebSocketProtocol::CloseCodeNormal, "Stop server"); + client->flush(); + client->abort(); } // Delete the server object - if (m_server) { - qCDebug(dcWebSocketServer()) << "Stop server" << m_server->serverName() << serverUrl().toString(); - m_server->close(); - delete m_server; - m_server = nullptr; - } + if (!m_server) + return true; + qCDebug(dcWebSocketServer()) << "Stop server" << m_server->serverName() << serverUrl().toString(); + m_server->close(); + delete m_server; + m_server = nullptr; return true; } diff --git a/libnymea-remoteproxy/websocketserver.h b/libnymea-remoteproxy/websocketserver.h index f4641d2..c6d172d 100644 --- a/libnymea-remoteproxy/websocketserver.h +++ b/libnymea-remoteproxy/websocketserver.h @@ -43,13 +43,10 @@ class WebSocketServer : public TransportInterface { Q_OBJECT public: - explicit WebSocketServer(const QSslConfiguration &sslConfiguration, QObject *parent = nullptr); + explicit WebSocketServer(bool sslEnabled, const QSslConfiguration &sslConfiguration, QObject *parent = nullptr); ~WebSocketServer() override; - QUrl serverUrl() const; - void setServerUrl(const QUrl &serverUrl); - - bool running() const; + bool running() const override; QSslConfiguration sslConfiguration() const; @@ -57,10 +54,9 @@ public: void killClientConnection(const QUuid &clientId, const QString &killReason) override; private: - QUrl m_serverUrl; QWebSocketServer *m_server = nullptr; + bool m_sslEnabled; QSslConfiguration m_sslConfiguration; - bool m_enabled = false; QHash m_clientList; diff --git a/libnymea-remoteproxyclient/libnymea-remoteproxyclient.pri b/libnymea-remoteproxyclient/libnymea-remoteproxyclient.pri index 2a7edce..d1d89c8 100644 --- a/libnymea-remoteproxyclient/libnymea-remoteproxyclient.pri +++ b/libnymea-remoteproxyclient/libnymea-remoteproxyclient.pri @@ -1,6 +1,7 @@ INCLUDEPATH += $${PWD} HEADERS += \ + $${PWD}/tcpsocketconnection.h \ $${PWD}/proxyjsonrpcclient.h \ $${PWD}/jsonreply.h \ $${PWD}/remoteproxyconnection.h \ @@ -8,6 +9,7 @@ HEADERS += \ $${PWD}/websocketconnection.h SOURCES += \ + $${PWD}/tcpsocketconnection.cpp \ $${PWD}/proxyjsonrpcclient.cpp \ $${PWD}/jsonreply.cpp \ $${PWD}/remoteproxyconnection.cpp \ diff --git a/libnymea-remoteproxyclient/proxyconnection.cpp b/libnymea-remoteproxyclient/proxyconnection.cpp index 075e32f..2dcffd7 100644 --- a/libnymea-remoteproxyclient/proxyconnection.cpp +++ b/libnymea-remoteproxyclient/proxyconnection.cpp @@ -34,6 +34,11 @@ ProxyConnection::ProxyConnection(QObject *parent) : QObject(parent) } +QUrl ProxyConnection::serverUrl() const +{ + return m_serverUrl; +} + bool ProxyConnection::connected() { return m_connected; @@ -48,6 +53,11 @@ void ProxyConnection::setConnected(bool connected) emit connectedChanged(m_connected); } +void ProxyConnection::setServerUrl(const QUrl &serverUrl) +{ + m_serverUrl = serverUrl; +} + ProxyConnection::~ProxyConnection() { diff --git a/libnymea-remoteproxyclient/proxyconnection.h b/libnymea-remoteproxyclient/proxyconnection.h index e23dd83..178fab7 100644 --- a/libnymea-remoteproxyclient/proxyconnection.h +++ b/libnymea-remoteproxyclient/proxyconnection.h @@ -28,6 +28,7 @@ #ifndef SOCKETCONNECTOR_H #define SOCKETCONNECTOR_H +#include #include #include #include @@ -43,7 +44,7 @@ public: virtual void sendData(const QByteArray &data) = 0; - virtual QUrl serverUrl() const = 0; + QUrl serverUrl() const; virtual void ignoreSslErrors() = 0; virtual void ignoreSslErrors(const QList &errors) = 0; @@ -52,9 +53,11 @@ public: private: bool m_connected = false; + QUrl m_serverUrl; protected: void setConnected(bool connected); + void setServerUrl(const QUrl &serverUrl); signals: void connectedChanged(bool connected); diff --git a/libnymea-remoteproxyclient/proxyjsonrpcclient.cpp b/libnymea-remoteproxyclient/proxyjsonrpcclient.cpp index 78b6550..962d7e7 100644 --- a/libnymea-remoteproxyclient/proxyjsonrpcclient.cpp +++ b/libnymea-remoteproxyclient/proxyjsonrpcclient.cpp @@ -73,10 +73,8 @@ void JsonRpcClient::sendRequest(const QVariantMap &request) m_connection->sendData(data); } -void JsonRpcClient::processData(const QByteArray &data) +void JsonRpcClient::processDataPackage(const QByteArray &data) { - qCDebug(dcRemoteProxyClientJsonRpcTraffic()) << "Received data:" << data; - QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { @@ -119,4 +117,22 @@ void JsonRpcClient::processData(const QByteArray &data) } } +void JsonRpcClient::processData(const QByteArray &data) +{ + qCDebug(dcRemoteProxyClientJsonRpcTraffic()) << "Received data:" << data; + + // Handle packet fragmentation + m_dataBuffer.append(data); + int splitIndex = m_dataBuffer.indexOf("}\n{"); + while (splitIndex > -1) { + processDataPackage(m_dataBuffer.left(splitIndex + 1)); + m_dataBuffer = m_dataBuffer.right(m_dataBuffer.length() - splitIndex - 2); + splitIndex = m_dataBuffer.indexOf("}\n{"); + } + if (m_dataBuffer.trimmed().endsWith("}")) { + processDataPackage(m_dataBuffer); + m_dataBuffer.clear(); + } +} + } diff --git a/libnymea-remoteproxyclient/proxyjsonrpcclient.h b/libnymea-remoteproxyclient/proxyjsonrpcclient.h index 8fed732..b16488b 100644 --- a/libnymea-remoteproxyclient/proxyjsonrpcclient.h +++ b/libnymea-remoteproxyclient/proxyjsonrpcclient.h @@ -54,10 +54,12 @@ private: ProxyConnection *m_connection = nullptr; int m_commandId = 0; + QByteArray m_dataBuffer; QHash m_replies; void sendRequest(const QVariantMap &request); + void processDataPackage(const QByteArray &data); signals: void tunnelEstablished(const QString clientName, const QString &clientUuid); diff --git a/libnymea-remoteproxyclient/remoteproxyconnection.cpp b/libnymea-remoteproxyclient/remoteproxyconnection.cpp index e636857..dde5028 100644 --- a/libnymea-remoteproxyclient/remoteproxyconnection.cpp +++ b/libnymea-remoteproxyclient/remoteproxyconnection.cpp @@ -27,6 +27,7 @@ #include "proxyconnection.h" #include "proxyjsonrpcclient.h" +#include "tcpsocketconnection.h" #include "websocketconnection.h" #include "remoteproxyconnection.h" @@ -43,6 +44,15 @@ RemoteProxyConnection::RemoteProxyConnection(const QUuid &clientUuid, const QStr } +RemoteProxyConnection::RemoteProxyConnection(const QUuid &clientUuid, const QString &clientName, RemoteProxyConnection::ConnectionType connectionType, QObject *parent) : + QObject(parent), + m_clientUuid(clientUuid), + m_clientName(clientName), + m_connectionType(connectionType) +{ + +} + RemoteProxyConnection::~RemoteProxyConnection() { @@ -309,25 +319,19 @@ void RemoteProxyConnection::onTunnelEstablished(const QString &clientName, const bool RemoteProxyConnection::connectServer(const QUrl &url) { - if (url.scheme() != "wss") { - // FIXME: support also tcp - qCWarning(dcRemoteProxyClientConnection()) << "Unsupported connection type" << url.scheme() << "Default to wss"; - m_serverUrl.setScheme("wss"); - } - m_serverUrl = url; - m_connectionType = ConnectionTypeWebSocket; m_error = QAbstractSocket::UnknownSocketError; cleanUp(); switch (m_connectionType) { case ConnectionTypeWebSocket: + qCDebug(dcRemoteProxyClientConnection()) << "Creating a web socket connection to" << url.toString(); m_connection = qobject_cast(new WebSocketConnection(this)); break; case ConnectionTypeTcpSocket: - // FIXME: - //m_connection = qobject_cast(new WebSocketConnection(this)); + qCDebug(dcRemoteProxyClientConnection()) << "Creating a TCP socket connection to" << url.toString(); + m_connection = qobject_cast(new TcpSocketConnection(this)); break; } diff --git a/libnymea-remoteproxyclient/remoteproxyconnection.h b/libnymea-remoteproxyclient/remoteproxyconnection.h index 28c92f6..1eeeb6d 100644 --- a/libnymea-remoteproxyclient/remoteproxyconnection.h +++ b/libnymea-remoteproxyclient/remoteproxyconnection.h @@ -69,6 +69,7 @@ public: Q_ENUM(State) explicit RemoteProxyConnection(const QUuid &clientUuid, const QString &clientName, QObject *parent = nullptr); + RemoteProxyConnection(const QUuid &clientUuid, const QString &clientName, ConnectionType connectionType, QObject *parent = nullptr); ~RemoteProxyConnection(); RemoteProxyConnection::State state() const; @@ -94,9 +95,9 @@ public: QString tunnelPartnerUuid() const; private: - ConnectionType m_connectionType = ConnectionTypeWebSocket; QUuid m_clientUuid; QString m_clientName; + ConnectionType m_connectionType = ConnectionTypeWebSocket; QUrl m_serverUrl; @@ -152,6 +153,7 @@ public slots: bool authenticate(const QString &token, const QString &nonce = QString()); void disconnectServer(); bool sendData(const QByteArray &data); + }; } diff --git a/libnymea-remoteproxyclient/tcpsocketconnection.cpp b/libnymea-remoteproxyclient/tcpsocketconnection.cpp new file mode 100644 index 0000000..213573b --- /dev/null +++ b/libnymea-remoteproxyclient/tcpsocketconnection.cpp @@ -0,0 +1,128 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "tcpsocketconnection.h" + +Q_LOGGING_CATEGORY(dcRemoteProxyClientTcpSocket, "RemoteProxyClientTcpSocket") + +namespace remoteproxyclient { + +TcpSocketConnection::TcpSocketConnection(QObject *parent) : + ProxyConnection(parent) +{ + m_tcpSocket = new QSslSocket(this); + + connect(m_tcpSocket, &QSslSocket::disconnected, this, &TcpSocketConnection::onDisconnected); + connect(m_tcpSocket, &QSslSocket::encrypted, this, &TcpSocketConnection::onEncrypted); + connect(m_tcpSocket, &QSslSocket::readyRead, this, &TcpSocketConnection::onReadyRead); + connect(m_tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError))); + connect(m_tcpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onStateChanged(QAbstractSocket::SocketState))); + connect(m_tcpSocket, SIGNAL(sslErrors(QList)), this, SIGNAL(sslErrors(QList))); +} + +TcpSocketConnection::~TcpSocketConnection() +{ + m_tcpSocket->close(); +} + +void TcpSocketConnection::sendData(const QByteArray &data) +{ + m_tcpSocket->write(data); +} + +void TcpSocketConnection::ignoreSslErrors() +{ + m_tcpSocket->ignoreSslErrors(); +} + +void TcpSocketConnection::ignoreSslErrors(const QList &errors) +{ + m_tcpSocket->ignoreSslErrors(errors); +} + +void TcpSocketConnection::onDisconnected() +{ + qCDebug(dcRemoteProxyClientTcpSocket()) << "Disconnected from" << serverUrl().toString(); + setConnected(false); +} + +void TcpSocketConnection::onEncrypted() +{ + qCDebug(dcRemoteProxyClientTcpSocket()) << "Connection encrypted"; + setConnected(true); +} + +void TcpSocketConnection::onError(QAbstractSocket::SocketError error) +{ + qCDebug(dcRemoteProxyClientTcpSocket()) << "Socket error occured" << error << m_tcpSocket->errorString(); + emit errorOccured(error); +} + +void TcpSocketConnection::onStateChanged(QAbstractSocket::SocketState state) +{ + qCDebug(dcRemoteProxyClientTcpSocket()) << "Socket state changed" << state; + + switch (state) { + case QAbstractSocket::ConnectedState: + qCDebug(dcRemoteProxyClientTcpSocket()) << "Connected with" << serverUrl().toString(); + if (!m_ssl) { + setConnected(true); + } + break; + default: + setConnected(false); + break; + } + + emit stateChanged(state); +} + +void TcpSocketConnection::onReadyRead() +{ + emit dataReceived(m_tcpSocket->readAll()); +} + +void TcpSocketConnection::connectServer(const QUrl &serverUrl) +{ + setServerUrl(serverUrl); + qCDebug(dcRemoteProxyClientTcpSocket()) << "Connecting to" << this->serverUrl().toString(); + + if (serverUrl.scheme() == "tcp") { + m_tcpSocket->connectToHost(QHostAddress(this->serverUrl().host()), static_cast(this->serverUrl().port())); + } else { + m_ssl = true; + m_tcpSocket->connectToHostEncrypted(this->serverUrl().host(), static_cast(this->serverUrl().port())); + } +} + +void TcpSocketConnection::disconnectServer() +{ + qCDebug(dcRemoteProxyClientTcpSocket()) << "Disconnecting from" << serverUrl().toString(); + m_tcpSocket->close(); +} + +} diff --git a/libnymea-remoteproxyclient/tcpsocketconnection.h b/libnymea-remoteproxyclient/tcpsocketconnection.h new file mode 100644 index 0000000..384a8dc --- /dev/null +++ b/libnymea-remoteproxyclient/tcpsocketconnection.h @@ -0,0 +1,74 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef TCPSOCKETCONNECTION_H +#define TCPSOCKETCONNECTION_H + +#include +#include +#include +#include + +#include "proxyconnection.h" + +Q_DECLARE_LOGGING_CATEGORY(dcRemoteProxyClienTcpSocket) + +namespace remoteproxyclient { + +class TcpSocketConnection : public ProxyConnection +{ + Q_OBJECT + +public: + explicit TcpSocketConnection(QObject *parent = nullptr); + ~TcpSocketConnection() override; + + void sendData(const QByteArray &data) override; + + void ignoreSslErrors() override; + void ignoreSslErrors(const QList &errors) override; + +private: + QSslSocket *m_tcpSocket = nullptr; + bool m_ssl = false; + +private slots: + void onDisconnected(); + void onEncrypted(); + void onError(QAbstractSocket::SocketError error); + void onStateChanged(QAbstractSocket::SocketState state); + void onReadyRead(); + +public slots: + void connectServer(const QUrl &serverUrl) override; + void disconnectServer() override; + +}; + +} + +#endif // TCPSOCKETCONNECTION_H diff --git a/libnymea-remoteproxyclient/websocketconnection.cpp b/libnymea-remoteproxyclient/websocketconnection.cpp index 5177cad..2b4dace 100644 --- a/libnymea-remoteproxyclient/websocketconnection.cpp +++ b/libnymea-remoteproxyclient/websocketconnection.cpp @@ -49,11 +49,6 @@ WebSocketConnection::~WebSocketConnection() m_webSocket->close(); } -QUrl WebSocketConnection::serverUrl() const -{ - return m_serverUrl; -} - void WebSocketConnection::sendData(const QByteArray &data) { m_webSocket->sendTextMessage(QString::fromUtf8(data + '\n')); @@ -108,10 +103,10 @@ void WebSocketConnection::connectServer(const QUrl &serverUrl) if (connected()) { m_webSocket->close(); } + setServerUrl(serverUrl); - m_serverUrl = serverUrl; - qCDebug(dcRemoteProxyClientWebSocket()) << "Connecting to" << m_serverUrl.toString(); - m_webSocket->open(m_serverUrl); + qCDebug(dcRemoteProxyClientWebSocket()) << "Connecting to" << this->serverUrl().toString(); + m_webSocket->open(this->serverUrl()); } void WebSocketConnection::disconnectServer() diff --git a/libnymea-remoteproxyclient/websocketconnection.h b/libnymea-remoteproxyclient/websocketconnection.h index cf7e771..252d0d9 100644 --- a/libnymea-remoteproxyclient/websocketconnection.h +++ b/libnymea-remoteproxyclient/websocketconnection.h @@ -46,15 +46,12 @@ public: explicit WebSocketConnection(QObject *parent = nullptr); ~WebSocketConnection() override; - QUrl serverUrl() const override; - void sendData(const QByteArray &data) override; void ignoreSslErrors() override; void ignoreSslErrors(const QList &errors) override; private: - QUrl m_serverUrl; QWebSocket *m_webSocket = nullptr; private slots: diff --git a/monitor/monitor.pro b/monitor/monitor.pro index d455aa8..983cb61 100644 --- a/monitor/monitor.pro +++ b/monitor/monitor.pro @@ -15,5 +15,5 @@ SOURCES += main.cpp \ LIBS += -lncurses -target.path = /usr/bin +target.path = $$[QT_INSTALL_PREFIX]/bin INSTALLS += target diff --git a/nymea-remoteproxy-logging.conf b/nymea-remoteproxy-logging.conf new file mode 100644 index 0000000..9dbac85 --- /dev/null +++ b/nymea-remoteproxy-logging.conf @@ -0,0 +1,16 @@ +[Rules] +*.debug=false +Application.debug=true +Engine.debug=true +Tunnel.debug=true +JsonRpc.debug=true +JsonRpcTraffic.debug=false +WebSocketServer.debug=true +WebSocketServerTraffic.debug=false +Authentication.debug=true +AuthenticationProcess.debug=false +ProxyServer.debug=true +ProxyServerTraffic.debug=false +MonitorServer.debug=true +AwsCredentialsProvider.debug=false +AwsCredentialsProviderTraffic.debug=false diff --git a/nymea-remoteproxy.conf b/nymea-remoteproxy.conf index 9266c60..639f33c 100644 --- a/nymea-remoteproxy.conf +++ b/nymea-remoteproxy.conf @@ -15,6 +15,7 @@ authorizerLambdaFunction=system-services-authorizer-dev-checkToken awsCredentialsUrl=http://169.254.169.254/latest/meta-data/iam/security-credentials/EC2-Remote-Connection-Proxy-Role [SSL] +enabled=true certificate=/etc/ssl/certs/ssl-cert-snakeoil.pem certificateKey=/etc/ssl/private/ssl-cert-snakeoil.key certificateChain= diff --git a/nymea-remoteproxy.pri b/nymea-remoteproxy.pri index 0d98588..38806df 100644 --- a/nymea-remoteproxy.pri +++ b/nymea-remoteproxy.pri @@ -5,7 +5,7 @@ QT -= gui SERVER_NAME=nymea-remoteproxy API_VERSION_MAJOR=0 API_VERSION_MINOR=3 -SERVER_VERSION=0.1.7 +SERVER_VERSION=0.1.8 DEFINES += SERVER_NAME_STRING=\\\"$${SERVER_NAME}\\\" \ SERVER_VERSION_STRING=\\\"$${SERVER_VERSION}\\\" \ @@ -23,7 +23,7 @@ ccache { QMAKE_CXX = ccache g++ } -coverage {< +coverage { # Note: this works only if you build in the source dir OBJECTS_DIR = MOC_DIR = diff --git a/nymea-remoteproxy.pro b/nymea-remoteproxy.pro index b42f003..2e38692 100644 --- a/nymea-remoteproxy.pro +++ b/nymea-remoteproxy.pro @@ -15,6 +15,11 @@ server.depends = libnymea-remoteproxy client.depends = libnymea-remoteproxyclient tests.depends = libnymea-remoteproxy libnymea-remoteproxyclient +test.commands = LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:$$top_builddir/libnymea-remoteproxy:$$top_builddir/libnymea-remoteproxyclient \ + LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:$$top_srcdir/libnymea-remoteproxy:$$top_srcdir/libnymea-remoteproxyclient \ + make check +QMAKE_EXTRA_TARGETS += test + message("----------------------------------------------------------") message("Building nymea-remoteproxy $${SERVER_VERSION}") message("----------------------------------------------------------") diff --git a/server/main.cpp b/server/main.cpp index a7057e8..1e95df4 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -62,19 +62,6 @@ static const char *const normal = "\033[0m"; static const char *const warning = "\e[33m"; static const char *const error = "\e[31m"; -static void loggingCategoryFilter(QLoggingCategory *category) -{ - if (s_loggingFilters.contains(category->categoryName())) { - bool debugEnabled = s_loggingFilters.value(category->categoryName()); - category->setEnabled(QtDebugMsg, debugEnabled); - category->setEnabled(QtWarningMsg, debugEnabled || s_loggingFilters.value("Warnings")); - } else { - // Enable default debug output - category->setEnabled(QtDebugMsg, true); - category->setEnabled(QtWarningMsg, s_loggingFilters.value("Warnings")); - } -} - static void consoleLogHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { QString messageString; @@ -119,22 +106,6 @@ int main(int argc, char *argv[]) application.setOrganizationName("nymea"); application.setApplicationVersion(SERVER_VERSION_STRING); - s_loggingFilters.insert("Application", true); - s_loggingFilters.insert("Engine", true); - s_loggingFilters.insert("JsonRpc", true); - s_loggingFilters.insert("WebSocketServer", true); - s_loggingFilters.insert("Authentication", true); - s_loggingFilters.insert("ProxyServer", true); - s_loggingFilters.insert("MonitorServer", true); - s_loggingFilters.insert("AwsCredentialsProvider", true); - - // Only with verbose enabled - s_loggingFilters.insert("JsonRpcTraffic", false); - s_loggingFilters.insert("ProxyServerTraffic", false); - s_loggingFilters.insert("AuthenticationProcess", false); - s_loggingFilters.insert("WebSocketServerTraffic", false); - s_loggingFilters.insert("AwsCredentialsProviderTraffic", false); - QString configFile = "/etc/nymea/nymea-remoteproxy.conf"; // command line parser @@ -145,7 +116,7 @@ int main(int argc, char *argv[]) "registered nymea deamons to establish a tunnel connection.\n\n" "Version: %1\n" "API version: %2\n\n" - "Copyright %3 2018 Simon Stürz \n") + "Copyright %3 2021 nymea GmbH \n") .arg(SERVER_VERSION_STRING) .arg(API_VERSION_STRING) .arg(QChar(0xA9))); @@ -186,15 +157,6 @@ int main(int argc, char *argv[]) configuration->setLogFileName(parser.value(logfileOption)); } - if (parser.isSet(verboseOption)) { - s_loggingFilters["JsonRpcTraffic"] = true; - s_loggingFilters["ProxyServerTraffic"] = true; - s_loggingFilters["AuthenticationProcess"] = true; - s_loggingFilters["WebSocketServerTraffic"] = true; - s_loggingFilters["AwsCredentialsProviderTraffic"] = true; - } - QLoggingCategory::installFilter(loggingCategoryFilter); - // Open logfile if configured if (configuration->writeLogFile()) { s_loggingEnabled = true; @@ -224,9 +186,11 @@ int main(int argc, char *argv[]) } // Verify SSL configuration - if (configuration->sslConfiguration().isNull()) { - qCCritical(dcApplication()) << "No SSL configuration specified. The server does not suppoert insecure connections."; + if (configuration->sslEnabled() && configuration->sslConfiguration().isNull()) { + qCCritical(dcApplication()) << "SSL is enabled but no SSL configuration specified."; exit(-1); + } else { + qCDebug(dcApplication()) << "Using SSL version:" << QSslSocket::sslLibraryVersionString(); } qCDebug(dcApplication()) << "=========================================================="; @@ -241,7 +205,6 @@ int main(int argc, char *argv[]) if (s_loggingEnabled) qCDebug(dcApplication()) << "Logging enabled. Writing logs to" << s_logFile.fileName(); - qCDebug(dcApplication()) << "Using SSL version:" << QSslSocket::sslLibraryVersionString(); Authenticator *authenticator = nullptr; if (parser.isSet(mockAuthenticatorOption)) { diff --git a/server/server.pro b/server/server.pro index eb044bb..2689bdf 100644 --- a/server/server.pro +++ b/server/server.pro @@ -10,7 +10,7 @@ LIBS += -L$$top_builddir/libnymea-remoteproxy/ -lnymea-remoteproxy SOURCES += main.cpp \ remoteproxyserverapplication.cpp -target.path = /usr/bin +target.path = $$[QT_INSTALL_PREFIX]/bin INSTALLS += target HEADERS += \ diff --git a/tests/resources/test-configuration.conf b/tests/resources/test-configuration.conf index e0fb21e..54bec2c 100644 --- a/tests/resources/test-configuration.conf +++ b/tests/resources/test-configuration.conf @@ -3,10 +3,10 @@ name=test-nymea-remoteproxy writeLogs=false logFile=/var/log/nymea-remoteproxy.log monitorSocket=/tmp/nymea-remoteproxy-test.sock -jsonRpcTimeout=1000 -authenticationTimeout=1500 +jsonRpcTimeout=2000 +authenticationTimeout=1000 inactiveTimeout=1500 -aloneTimeout=1000 +aloneTimeout=1500 [SSL] certificate=:/test-certificate.crt diff --git a/tests/test-offline/nymea-remoteproxy-tests-offline.cpp b/tests/test-offline/nymea-remoteproxy-tests-offline.cpp index 77d166b..8d373ab 100644 --- a/tests/test-offline/nymea-remoteproxy-tests-offline.cpp +++ b/tests/test-offline/nymea-remoteproxy-tests-offline.cpp @@ -53,9 +53,14 @@ void RemoteProxyOfflineTests::dummyAuthenticator() { cleanUpEngine(); + m_configuration = new ProxyConfiguration(this); + loadConfiguration(":/test-configuration.conf"); + + m_dummyAuthenticator = new DummyAuthenticator(this); + m_authenticator = qobject_cast(m_dummyAuthenticator); + // Start proxy webserver Engine::instance()->setAuthenticator(m_dummyAuthenticator); - Engine::instance()->setAuthenticator(m_dummyAuthenticator); QSignalSpy runningSpy(Engine::instance(), &Engine::runningChanged); Engine::instance()->start(m_configuration); runningSpy.wait(); @@ -73,7 +78,7 @@ void RemoteProxyOfflineTests::dummyAuthenticator() params.insert("name", "test"); params.insert("token", "foobar"); - QVariant response = invokeApiCall("Authentication.Authenticate", params); + QVariant response = invokeWebSocketApiCall("Authentication.Authenticate", params); qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); verifyAuthenticationError(response); @@ -209,16 +214,21 @@ void RemoteProxyOfflineTests::serverPortBlocked() { cleanUpEngine(); + m_configuration = new ProxyConfiguration(this); + loadConfiguration(":/test-configuration.conf"); + + m_mockAuthenticator = new MockAuthenticator(this); + m_authenticator = qobject_cast(m_mockAuthenticator); + // Create a dummy server which blocks the port QWebSocketServer dummyServer("dummy-server", QWebSocketServer::NonSecureMode); - dummyServer.listen(QHostAddress::LocalHost, 1212); + QVERIFY(dummyServer.listen(QHostAddress::LocalHost, m_configuration->webSocketServerPort())); // Start proxy webserver QSignalSpy runningSpy(Engine::instance(), &Engine::runningChanged); Engine::instance()->setAuthenticator(m_authenticator); Engine::instance()->start(m_configuration); runningSpy.wait(); - qDebug() << runningSpy.count(); QVERIFY(runningSpy.count() == 1); // Make sure the server is not running @@ -233,11 +243,49 @@ void RemoteProxyOfflineTests::serverPortBlocked() QVERIFY(closedSpy.count() == 1); // Try again - cleanUpEngine(); startServer(); // Clean up stopServer(); + + // Do the same with the tcp server + cleanUpEngine(); + + m_configuration = new ProxyConfiguration(this); + loadConfiguration(":/test-configuration.conf"); + + m_mockAuthenticator = new MockAuthenticator(this); + m_authenticator = qobject_cast(m_mockAuthenticator); + + // Create a dummy server which blocks the port + QTcpServer *tcpDummyServer = new QTcpServer(this); + QVERIFY(tcpDummyServer->listen(QHostAddress::LocalHost, m_configuration->tcpServerPort())); + + // Start proxy webserver + QSignalSpy runningSpy2(Engine::instance(), &Engine::runningChanged); + Engine::instance()->setAuthenticator(m_authenticator); + Engine::instance()->start(m_configuration); + runningSpy2.wait(); + QVERIFY(runningSpy2.count() == 1); + + // Make sure the engine is running + QVERIFY(Engine::instance()->running()); + + // Make sure the TCP server is not running + QVERIFY(!Engine::instance()->tcpSocketServer()->running()); + + tcpDummyServer->close(); + delete tcpDummyServer; + + // Try again + startServer(); + + // Make sure the TCP server is not running + QVERIFY(Engine::instance()->webSocketServer()->running()); + QVERIFY(Engine::instance()->tcpSocketServer()->running()); + + // Clean up + stopServer(); } void RemoteProxyOfflineTests::websocketBinaryData() @@ -286,13 +334,70 @@ void RemoteProxyOfflineTests::websocketPing() stopServer(); } +//void RemoteProxyOfflineTests::apiBasicCallsTcp_data() +//{ +// QTest::addColumn("data"); +// QTest::addColumn("responseId"); +// QTest::addColumn("responseStatus"); + +// QTest::newRow("valid call") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Hello\"}") << 42 << "success"; +// QTest::newRow("missing id") << QByteArray("{\"method\":\"RemoteProxy.Hello\"}") << -1 << "error"; +// QTest::newRow("missing method") << QByteArray("{\"id\":42}") << 42 << "error"; +// QTest::newRow("invalid json") << QByteArray("{\"id\":42, \"method\":\"RemoteProx") << -1 << "error"; +// QTest::newRow("invalid function") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Explode\"}") << 42 << "error"; +// QTest::newRow("invalid namespace") << QByteArray("{\"id\":42, \"method\":\"ProxyRemote.Hello\"}") << 42 << "error"; +// QTest::newRow("missing dot") << QByteArray("{\"id\":42, \"method\":\"RemoteProxyHello\"}") << 42 << "error"; +// QTest::newRow("invalid params") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Hello\", \"params\":{\"törööö\":\"chooo-chooo\"}}") << 42 << "error"; +// QTest::newRow("invalid authentication params") << QByteArray("{\"id\":42, \"method\":\"Authentication.Authenticate\", \"params\":{\"your\":\"mamma\"}}") << 42 << "error"; +//} + +//void RemoteProxyOfflineTests::apiBasicCallsTcp() +//{ +// QFETCH(QByteArray, data); +// QFETCH(int, responseId); +// QFETCH(QString, responseStatus); + +// // Start the server +// startServer(); + +// QVariant response = injectTcpSocketData(data); +// QVERIFY(!response.isNull()); + +// qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + +// QCOMPARE(response.toMap().value("id").toInt(), responseId); +// QCOMPARE(response.toMap().value("status").toString(), responseStatus); + +// // Clean up +// stopServer(); +//} + void RemoteProxyOfflineTests::getIntrospect() { // Start the server startServer(); - QVariant response = invokeApiCall("RemoteProxy.Introspect"); - qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + QVariantMap response; + + // WebSocket + response = invokeWebSocketApiCall("RemoteProxy.Introspect").toMap(); + //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + QVERIFY(!response.isEmpty()); + QVERIFY(response.value("status").toString() == "success"); + QVERIFY(response.value("params").toMap().contains("methods")); + QVERIFY(response.value("params").toMap().contains("notifications")); + QVERIFY(response.value("params").toMap().contains("types")); + + // Tcp + response.clear(); + response = invokeTcpSocketApiCall("RemoteProxy.Introspect").toMap(); + //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + + QVERIFY(!response.isEmpty()); + QVERIFY(response.value("status").toString() == "success"); + QVERIFY(response.value("params").toMap().contains("methods")); + QVERIFY(response.value("params").toMap().contains("notifications")); + QVERIFY(response.value("params").toMap().contains("types")); // Clean up stopServer(); @@ -302,16 +407,32 @@ void RemoteProxyOfflineTests::getHello() { // Start the server startServer(); + QVariantMap response; - QVariantMap response = invokeApiCall("RemoteProxy.Hello").toMap(); - qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + // WebSocket + response = invokeWebSocketApiCall("RemoteProxy.Hello").toMap(); + //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); // Verify data + QVERIFY(!response.isEmpty()); QCOMPARE(response.value("params").toMap().value("name").toString(), Engine::instance()->configuration()->serverName()); QCOMPARE(response.value("params").toMap().value("server").toString(), QString(SERVER_NAME_STRING)); QCOMPARE(response.value("params").toMap().value("version").toString(), QString(SERVER_VERSION_STRING)); QCOMPARE(response.value("params").toMap().value("apiVersion").toString(), QString(API_VERSION_STRING)); + // TCP + response.clear(); + response = invokeTcpSocketApiCall("RemoteProxy.Hello").toMap(); + //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + + // Verify data + QVERIFY(!response.isEmpty()); + QCOMPARE(response.value("params").toMap().value("name").toString(), Engine::instance()->configuration()->serverName()); + QCOMPARE(response.value("params").toMap().value("server").toString(), QString(SERVER_NAME_STRING)); + QCOMPARE(response.value("params").toMap().value("version").toString(), QString(SERVER_VERSION_STRING)); + QCOMPARE(response.value("params").toMap().value("apiVersion").toString(), QString(API_VERSION_STRING)); + + // Clean up stopServer(); } @@ -325,7 +446,7 @@ void RemoteProxyOfflineTests::apiBasicCalls_data() QTest::newRow("valid call") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Hello\"}") << 42 << "success"; QTest::newRow("missing id") << QByteArray("{\"method\":\"RemoteProxy.Hello\"}") << -1 << "error"; QTest::newRow("missing method") << QByteArray("{\"id\":42}") << 42 << "error"; - QTest::newRow("invalid json") << QByteArray("{\"id\":42, \"method\":\"RemoteProx") << -1 << "error"; + QTest::newRow("invalid json") << QByteArray("{\"id\":42, \"method\":\"RemoteProx}") << -1 << "error"; QTest::newRow("invalid function") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Explode\"}") << 42 << "error"; QTest::newRow("invalid namespace") << QByteArray("{\"id\":42, \"method\":\"ProxyRemote.Hello\"}") << 42 << "error"; QTest::newRow("missing dot") << QByteArray("{\"id\":42, \"method\":\"RemoteProxyHello\"}") << 42 << "error"; @@ -342,11 +463,22 @@ void RemoteProxyOfflineTests::apiBasicCalls() // Start the server startServer(); - QVariant response = injectSocketData(data); - qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + QVariantMap response; - QCOMPARE(response.toMap().value("id").toInt(), responseId); - QCOMPARE(response.toMap().value("status").toString(), responseStatus); + // Websocket + response = injectWebSocketData(data).toMap(); + //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + QVERIFY(!response.isEmpty()); + QCOMPARE(response.value("id").toInt(), responseId); + QCOMPARE(response.value("status").toString(), responseStatus); + + // TCP + response.clear(); + response = injectTcpSocketData(data).toMap(); + //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + QVERIFY(!response.isEmpty()); + QCOMPARE(response.value("id").toInt(), responseId); + QCOMPARE(response.value("status").toString(), responseStatus); // Clean up stopServer(); @@ -409,8 +541,14 @@ void RemoteProxyOfflineTests::authenticate() params.insert("token", token); if (!nonce.isEmpty()) params.insert("nonce", nonce); - QVariant response = invokeApiCall("Authentication.Authenticate", params); - qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + // WebSocket + QVariantMap response; + response = invokeWebSocketApiCall("Authentication.Authenticate", params).toMap(); + //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + verifyAuthenticationError(response, expectedError); + + // TCP + response = invokeTcpSocketApiCall("Authentication.Authenticate", params).toMap(); verifyAuthenticationError(response, expectedError); // Clean up @@ -518,6 +656,9 @@ void RemoteProxyOfflineTests::authenticateSendData() // Start the server startServer(); + m_mockAuthenticator->setTimeoutDuration(100); + m_mockAuthenticator->setExpectedAuthenticationError(); + QVariantMap params; params.insert("uuid", "uuid"); params.insert("name", "name"); @@ -533,18 +674,18 @@ void RemoteProxyOfflineTests::authenticateSendData() // Connect socket QWebSocket *socket = new QWebSocket("proxy-testclient", QWebSocketProtocol::Version13); connect(socket, &QWebSocket::sslErrors, this, &BaseTest::sslErrors); - QSignalSpy spyConnection(socket, SIGNAL(connected())); + QSignalSpy spyConnection(socket, &QWebSocket::connected); socket->open(Engine::instance()->webSocketServer()->serverUrl()); spyConnection.wait(); QVERIFY(spyConnection.count() == 1); // Authenticate - QSignalSpy dataSpy(socket, SIGNAL(textMessageReceived(QString))); + QSignalSpy dataSpy(socket, &QWebSocket::textMessageReceived); socket->sendTextMessage(QString(jsonDoc.toJson(QJsonDocument::Compact))); dataSpy.wait(); QVERIFY(dataSpy.count() == 1); - // Send data and make sure we get disconnected + // Send data again and make sure we get disconnected since sending data while waiting for the partner is forbidden QSignalSpy disconnectedSpy(socket, SIGNAL(disconnected())); socket->sendTextMessage(QString(jsonDoc.toJson(QJsonDocument::Compact))); disconnectedSpy.wait(); @@ -556,7 +697,7 @@ void RemoteProxyOfflineTests::authenticateSendData() stopServer(); } -void RemoteProxyOfflineTests::clientConnection() +void RemoteProxyOfflineTests::clientConnectionWebSocket() { // Start the server startServer(); @@ -565,7 +706,7 @@ void RemoteProxyOfflineTests::clientConnection() m_mockAuthenticator->setTimeoutDuration(100); m_mockAuthenticator->setExpectedAuthenticationError(); - // Connect to the server (insecure disabled) + // Connect to the server using WebSocket (insecure disabled) RemoteProxyConnection *connection = new RemoteProxyConnection(QUuid::createUuid(), "Test client one", this); connect(connection, &RemoteProxyConnection::sslErrors, this, &BaseTest::ignoreConnectionSslError); @@ -606,6 +747,56 @@ void RemoteProxyOfflineTests::clientConnection() stopServer(); } +void RemoteProxyOfflineTests::clientConnectionTcpSocket() +{ + // Start the server + startServer(); + + // Configure mock authenticator + m_mockAuthenticator->setTimeoutDuration(100); + m_mockAuthenticator->setExpectedAuthenticationError(); + + // Connect to the server using TCP (insecure disabled) + RemoteProxyConnection *connection = new RemoteProxyConnection(QUuid::createUuid(), "Test client one", RemoteProxyConnection::ConnectionTypeTcpSocket, this); + connect(connection, &RemoteProxyConnection::sslErrors, this, &BaseTest::ignoreConnectionSslError); + + // Connect to server (insecue enabled for testing) + QSignalSpy readySpy(connection, &RemoteProxyConnection::ready); + QVERIFY(connection->connectServer(m_serverUrlTcp)); + readySpy.wait(); + QVERIFY(readySpy.count() == 1); + QVERIFY(connection->isConnected()); + QVERIFY(!connection->isRemoteConnected()); + QVERIFY(connection->state() == RemoteProxyConnection::StateReady); + QVERIFY(connection->error() == QAbstractSocket::UnknownSocketError); + QVERIFY(connection->serverUrl() == m_serverUrlTcp); + QVERIFY(connection->connectionType() == RemoteProxyConnection::ConnectionTypeTcpSocket); + QVERIFY(connection->serverName() == SERVER_NAME_STRING); + QVERIFY(connection->proxyServerName() == Engine::instance()->serverName()); + QVERIFY(connection->proxyServerVersion() == SERVER_VERSION_STRING); + QVERIFY(connection->proxyServerApiVersion() == API_VERSION_STRING); + + QSignalSpy authenticatedSpy(connection, &RemoteProxyConnection::authenticated); + QVERIFY(connection->authenticate("foobar")); + authenticatedSpy.wait(); + QVERIFY(authenticatedSpy.count() == 1); + QVERIFY(connection->isConnected()); + QVERIFY(connection->isAuthenticated()); + QVERIFY(connection->state() == RemoteProxyConnection::StateAuthenticated); + + // Disconnect and clean up + QSignalSpy spyDisconnected(connection, &RemoteProxyConnection::disconnected); + connection->disconnectServer(); + // FIXME: check why it waits the full time here + spyDisconnected.wait(500); + + QVERIFY(spyDisconnected.count() >= 1); + QVERIFY(!connection->isConnected()); + + connection->deleteLater(); + stopServer(); +} + void RemoteProxyOfflineTests::remoteConnection() { // Start the server @@ -912,6 +1103,10 @@ void RemoteProxyOfflineTests::jsonRpcTimeout() // Start the server startServer(); + m_configuration->setAuthenticationTimeout(3000); + m_configuration->setJsonRpcTimeout(1000); + m_configuration->setInactiveTimeout(2000); + // Configure result (authentication takes longer than json rpc timeout m_mockAuthenticator->setExpectedAuthenticationError(); m_mockAuthenticator->setTimeoutDuration(4000); @@ -925,7 +1120,7 @@ void RemoteProxyOfflineTests::jsonRpcTimeout() params.insert("name", "name"); params.insert("token", "token"); - QVariant response = invokeApiCall("Authentication.Authenticate", params); + QVariant response = invokeWebSocketApiCall("Authentication.Authenticate", params); qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); QVERIFY(response.toMap().value("status").toString() == "error"); @@ -964,7 +1159,7 @@ void RemoteProxyOfflineTests::authenticationReplyTimeout() // Configure result (authentication takes longer than json rpc timeout m_mockAuthenticator->setExpectedAuthenticationError(); - m_mockAuthenticator->setTimeoutDuration(1000); + m_mockAuthenticator->setTimeoutDuration(2000); m_configuration->setAuthenticationTimeout(500); m_configuration->setJsonRpcTimeout(1000); @@ -976,7 +1171,7 @@ void RemoteProxyOfflineTests::authenticationReplyTimeout() params.insert("name", "Sleepy test client"); params.insert("token", "sleepy token zzzZZZ"); - QVariant response = invokeApiCall("Authentication.Authenticate", params); + QVariant response = invokeWebSocketApiCall("Authentication.Authenticate", params); qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); verifyAuthenticationError(response, Authenticator::AuthenticationErrorTimeout); @@ -1017,4 +1212,192 @@ void RemoteProxyOfflineTests::authenticationReplyConnection() stopServer(); } +void RemoteProxyOfflineTests::tcpRemoteConnection() +{ + // Start the server + startServer(); + + // Configure mock authenticator + m_mockAuthenticator->setTimeoutDuration(100); + m_mockAuthenticator->setExpectedAuthenticationError(); + + QString nameConnectionOne = "Test client one"; + QUuid uuidConnectionOne = QUuid::createUuid(); + + QString nameConnectionTwo = "Test client two"; + QUuid uuidConnectionTwo = QUuid::createUuid(); + + QByteArray dataOne = "Hello from client one :-)"; + QByteArray dataTwo = "Hello from client two :-)"; + + // Create two connection + RemoteProxyConnection *connectionOne = new RemoteProxyConnection(uuidConnectionOne, nameConnectionOne, RemoteProxyConnection::ConnectionTypeTcpSocket, this); + connect(connectionOne, &RemoteProxyConnection::sslErrors, this, &BaseTest::ignoreConnectionSslError); + + RemoteProxyConnection *connectionTwo = new RemoteProxyConnection(uuidConnectionTwo, nameConnectionTwo, RemoteProxyConnection::ConnectionTypeTcpSocket, this); + connect(connectionTwo, &RemoteProxyConnection::sslErrors, this, &BaseTest::ignoreConnectionSslError); + + // Connect one + QSignalSpy connectionOneReadySpy(connectionOne, &RemoteProxyConnection::ready); + QVERIFY(connectionOne->connectServer(m_serverUrlTcp)); + connectionOneReadySpy.wait(); + QVERIFY(connectionOneReadySpy.count() == 1); + QVERIFY(connectionOne->isConnected()); + + // Connect two + QSignalSpy connectionTwoReadySpy(connectionTwo, &RemoteProxyConnection::ready); + QVERIFY(connectionTwo->connectServer(m_serverUrlTcp)); + connectionTwoReadySpy.wait(); + QVERIFY(connectionTwoReadySpy.count() == 1); + QVERIFY(connectionTwo->isConnected()); + + // Authenticate one + QSignalSpy remoteConnectionEstablishedOne(connectionOne, &RemoteProxyConnection::remoteConnectionEstablished); + QSignalSpy connectionOneAuthenticatedSpy(connectionOne, &RemoteProxyConnection::authenticated); + QVERIFY(connectionOne->authenticate(m_testToken)); + connectionOneAuthenticatedSpy.wait(); + QVERIFY(connectionOneAuthenticatedSpy.count() == 1); + QVERIFY(connectionOne->isConnected()); + QVERIFY(connectionOne->isAuthenticated()); + QVERIFY(connectionOne->state() == RemoteProxyConnection::StateAuthenticated); + + // Authenticate two + QSignalSpy remoteConnectionEstablishedTwo(connectionTwo, &RemoteProxyConnection::remoteConnectionEstablished); + QSignalSpy connectionTwoAuthenticatedSpy(connectionTwo, &RemoteProxyConnection::authenticated); + QVERIFY(connectionTwo->authenticate(m_testToken)); + connectionTwoAuthenticatedSpy.wait(); + qDebug() << connectionTwoAuthenticatedSpy.count(); + QVERIFY(connectionTwoAuthenticatedSpy.count() == 1); + QVERIFY(connectionTwo->isConnected()); + QVERIFY(connectionTwo->isAuthenticated()); + + // Wait for both to be connected + remoteConnectionEstablishedOne.wait(500); + remoteConnectionEstablishedTwo.wait(500); + + QVERIFY(remoteConnectionEstablishedOne.count() == 1); + QVERIFY(remoteConnectionEstablishedTwo.count() == 1); + QVERIFY(connectionOne->state() == RemoteProxyConnection::StateRemoteConnected); + QVERIFY(connectionTwo->state() == RemoteProxyConnection::StateRemoteConnected); + + QCOMPARE(connectionOne->tunnelPartnerName(), nameConnectionTwo); + QCOMPARE(connectionOne->tunnelPartnerUuid(), uuidConnectionTwo.toString()); + QCOMPARE(connectionTwo->tunnelPartnerName(), nameConnectionOne); + QCOMPARE(connectionTwo->tunnelPartnerUuid(), uuidConnectionOne.toString()); + + // Pipe data trought the tunnel + QSignalSpy remoteConnectionDataOne(connectionOne, &RemoteProxyConnection::dataReady); + QSignalSpy remoteConnectionDataTwo(connectionTwo, &RemoteProxyConnection::dataReady); + + connectionOne->sendData(dataOne); + remoteConnectionDataTwo.wait(500); + QVERIFY(remoteConnectionDataTwo.count() == 1); + QCOMPARE(remoteConnectionDataTwo.at(0).at(0).toByteArray().trimmed(), dataOne); + + connectionTwo->sendData(dataTwo); + remoteConnectionDataOne.wait(500); + QVERIFY(remoteConnectionDataOne.count() == 1); + QCOMPARE(remoteConnectionDataOne.at(0).at(0).toByteArray().trimmed(), dataTwo); + + connectionOne->deleteLater(); + connectionTwo->deleteLater(); + + // Clean up + stopServer(); +} + +void RemoteProxyOfflineTests::tcpWebsocketRemoteConnection() +{ + // Start the server + startServer(); + + // Configure mock authenticator + m_mockAuthenticator->setTimeoutDuration(100); + m_mockAuthenticator->setExpectedAuthenticationError(); + + QString nameConnectionOne = "Test client one"; + QUuid uuidConnectionOne = QUuid::createUuid(); + + QString nameConnectionTwo = "Test client two"; + QUuid uuidConnectionTwo = QUuid::createUuid(); + + QByteArray dataOne = "Hello from client one :-)"; + QByteArray dataTwo = "Hello from client two :-)"; + + // Create two connection + RemoteProxyConnection *connectionOne = new RemoteProxyConnection(uuidConnectionOne, nameConnectionOne, RemoteProxyConnection::ConnectionTypeWebSocket, this); + connect(connectionOne, &RemoteProxyConnection::sslErrors, this, &BaseTest::ignoreConnectionSslError); + + RemoteProxyConnection *connectionTwo = new RemoteProxyConnection(uuidConnectionTwo, nameConnectionTwo, RemoteProxyConnection::ConnectionTypeTcpSocket, this); + connect(connectionTwo, &RemoteProxyConnection::sslErrors, this, &BaseTest::ignoreConnectionSslError); + + // Connect one + QSignalSpy connectionOneReadySpy(connectionOne, &RemoteProxyConnection::ready); + QVERIFY(connectionOne->connectServer(m_serverUrl)); + connectionOneReadySpy.wait(); + QVERIFY(connectionOneReadySpy.count() == 1); + QVERIFY(connectionOne->isConnected()); + + // Connect two + QSignalSpy connectionTwoReadySpy(connectionTwo, &RemoteProxyConnection::ready); + QVERIFY(connectionTwo->connectServer(m_serverUrlTcp)); + connectionTwoReadySpy.wait(); + QVERIFY(connectionTwoReadySpy.count() == 1); + QVERIFY(connectionTwo->isConnected()); + + // Authenticate one + QSignalSpy remoteConnectionEstablishedOne(connectionOne, &RemoteProxyConnection::remoteConnectionEstablished); + QSignalSpy connectionOneAuthenticatedSpy(connectionOne, &RemoteProxyConnection::authenticated); + QVERIFY(connectionOne->authenticate(m_testToken)); + connectionOneAuthenticatedSpy.wait(); + QVERIFY(connectionOneAuthenticatedSpy.count() == 1); + QVERIFY(connectionOne->isConnected()); + QVERIFY(connectionOne->isAuthenticated()); + QVERIFY(connectionOne->state() == RemoteProxyConnection::StateAuthenticated); + + // Authenticate two + QSignalSpy remoteConnectionEstablishedTwo(connectionTwo, &RemoteProxyConnection::remoteConnectionEstablished); + QSignalSpy connectionTwoAuthenticatedSpy(connectionTwo, &RemoteProxyConnection::authenticated); + QVERIFY(connectionTwo->authenticate(m_testToken)); + connectionTwoAuthenticatedSpy.wait(); + qDebug() << connectionTwoAuthenticatedSpy.count(); + QVERIFY(connectionTwoAuthenticatedSpy.count() == 1); + QVERIFY(connectionTwo->isConnected()); + QVERIFY(connectionTwo->isAuthenticated()); + + // Wait for both to be connected + remoteConnectionEstablishedOne.wait(500); + remoteConnectionEstablishedTwo.wait(500); + + QVERIFY(remoteConnectionEstablishedOne.count() == 1); + QVERIFY(remoteConnectionEstablishedTwo.count() == 1); + QVERIFY(connectionOne->state() == RemoteProxyConnection::StateRemoteConnected); + QVERIFY(connectionTwo->state() == RemoteProxyConnection::StateRemoteConnected); + + QCOMPARE(connectionOne->tunnelPartnerName(), nameConnectionTwo); + QCOMPARE(connectionOne->tunnelPartnerUuid(), uuidConnectionTwo.toString()); + QCOMPARE(connectionTwo->tunnelPartnerName(), nameConnectionOne); + QCOMPARE(connectionTwo->tunnelPartnerUuid(), uuidConnectionOne.toString()); + + // Pipe data trought the tunnel + QSignalSpy remoteConnectionDataOne(connectionOne, &RemoteProxyConnection::dataReady); + QSignalSpy remoteConnectionDataTwo(connectionTwo, &RemoteProxyConnection::dataReady); + + connectionOne->sendData(dataOne); + remoteConnectionDataTwo.wait(500); + QVERIFY(remoteConnectionDataTwo.count() == 1); + QCOMPARE(remoteConnectionDataTwo.at(0).at(0).toByteArray().trimmed(), dataOne); + + connectionTwo->sendData(dataTwo); + remoteConnectionDataOne.wait(500); + QVERIFY(remoteConnectionDataOne.count() == 1); + QCOMPARE(remoteConnectionDataOne.at(0).at(0).toByteArray().trimmed(), dataTwo); + + connectionOne->deleteLater(); + connectionTwo->deleteLater(); + + // Clean up + stopServer(); +} + QTEST_MAIN(RemoteProxyOfflineTests) diff --git a/tests/test-offline/nymea-remoteproxy-tests-offline.h b/tests/test-offline/nymea-remoteproxy-tests-offline.h index aad9c69..dc0a233 100644 --- a/tests/test-offline/nymea-remoteproxy-tests-offline.h +++ b/tests/test-offline/nymea-remoteproxy-tests-offline.h @@ -54,7 +54,7 @@ private slots: void websocketBinaryData(); void websocketPing(); - // Api + // WebSocket connection API void getIntrospect(); void getHello(); @@ -68,18 +68,23 @@ private slots: void authenticateSendData(); // Client lib - void clientConnection(); + void clientConnectionTcpSocket(); + void clientConnectionWebSocket(); void remoteConnection(); void multipleRemoteConnection(); void trippleConnection(); void duplicateUuid(); void sslConfigurations(); - void jsonRpcTimeout(); void inactiveTimeout(); + void jsonRpcTimeout(); void authenticationReplyTimeout(); void authenticationReplyConnection(); + // TCP Websocket combinations + void tcpRemoteConnection(); + void tcpWebsocketRemoteConnection(); + }; #endif // NYMEA_REMOTEPROXY_TESTS_OFFLINE_H diff --git a/tests/test-offline/test-offline.pro b/tests/test-offline/test-offline.pro index 51e64f3..c6db854 100644 --- a/tests/test-offline/test-offline.pro +++ b/tests/test-offline/test-offline.pro @@ -10,5 +10,5 @@ HEADERS += nymea-remoteproxy-tests-offline.h SOURCES += nymea-remoteproxy-tests-offline.cpp -target.path = /usr/bin +target.path = $$[QT_INSTALL_PREFIX]/bin INSTALLS += target diff --git a/tests/test-online/test-online.pro b/tests/test-online/test-online.pro index 30c87ab..9f20b41 100644 --- a/tests/test-online/test-online.pro +++ b/tests/test-online/test-online.pro @@ -7,5 +7,5 @@ HEADERS += nymea-remoteproxy-tests-online.h SOURCES += nymea-remoteproxy-tests-online.cpp -target.path = /usr/bin +target.path = $$[QT_INSTALL_PREFIX]/bin INSTALLS += target diff --git a/tests/testbase/basetest.cpp b/tests/testbase/basetest.cpp index 1a52857..42ecbcc 100644 --- a/tests/testbase/basetest.cpp +++ b/tests/testbase/basetest.cpp @@ -31,8 +31,10 @@ #include "loggingcategories.h" #include "remoteproxyconnection.h" +#include #include #include +#include #include #include #include @@ -47,16 +49,34 @@ void BaseTest::loadConfiguration(const QString &fileName) { qDebug() << "Load test configurations" << fileName; m_configuration->loadConfiguration(fileName); - restartEngine(); + //restartEngine(); } void BaseTest::cleanUpEngine() { + qDebug() << "Clean up engine"; if (Engine::exists()) { Engine::instance()->stop(); Engine::instance()->destroy(); QVERIFY(!Engine::exists()); } + + if (m_mockAuthenticator) { + delete m_mockAuthenticator; + m_mockAuthenticator = nullptr; + } + + if (m_dummyAuthenticator) { + delete m_dummyAuthenticator; + m_dummyAuthenticator = nullptr; + } + + m_authenticator = nullptr; + + if (m_configuration) { + delete m_configuration; + m_configuration = nullptr; + } } void BaseTest::restartEngine() @@ -67,28 +87,36 @@ void BaseTest::restartEngine() void BaseTest::startEngine() { + m_configuration = new ProxyConfiguration(this); + loadConfiguration(":/test-configuration.conf"); + + m_dummyAuthenticator = new DummyAuthenticator(this); + m_mockAuthenticator = new MockAuthenticator(this); + m_authenticator = qobject_cast(m_mockAuthenticator); if (!Engine::exists()) { Engine::instance()->setAuthenticator(m_authenticator); Engine::instance()->setDeveloperModeEnabled(true); QVERIFY(Engine::exists()); + QVERIFY(!Engine::instance()->running()); } } void BaseTest::startServer() { - startEngine(); + restartEngine(); if (!Engine::instance()->running()) { QSignalSpy runningSpy(Engine::instance(), &Engine::runningChanged); Engine::instance()->setDeveloperModeEnabled(true); Engine::instance()->start(m_configuration); - runningSpy.wait(100); + runningSpy.wait(200); QVERIFY(runningSpy.count() == 1); } QVERIFY(Engine::instance()->running()); QVERIFY(Engine::instance()->developerMode()); QVERIFY(Engine::instance()->webSocketServer()->running()); + QVERIFY(Engine::instance()->tcpSocketServer()->running()); QVERIFY(Engine::instance()->monitorServer()->running()); } @@ -99,9 +127,11 @@ void BaseTest::stopServer() Engine::instance()->stop(); QVERIFY(!Engine::instance()->running()); + + cleanUpEngine(); } -QVariant BaseTest::invokeApiCall(const QString &method, const QVariantMap params, bool remainsConnected) +QVariant BaseTest::invokeWebSocketApiCall(const QString &method, const QVariantMap params, bool remainsConnected) { Q_UNUSED(remainsConnected) @@ -152,11 +182,12 @@ QVariant BaseTest::invokeApiCall(const QString &method, const QVariantMap params return jsonDoc.toVariant(); } } + m_commandCounter++; return QVariant(); } -QVariant BaseTest::injectSocketData(const QByteArray &data) +QVariant BaseTest::injectWebSocketData(const QByteArray &data) { QWebSocket *socket = new QWebSocket("proxy-testclient", QWebSocketProtocol::Version13); connect(socket, &QWebSocket::sslErrors, this, &BaseTest::sslErrors); @@ -195,6 +226,66 @@ QVariant BaseTest::injectSocketData(const QByteArray &data) return QVariant(); } +QVariant BaseTest::invokeTcpSocketApiCall(const QString &method, const QVariantMap params, bool remainsConnected) +{ + Q_UNUSED(remainsConnected) + + QVariantMap request; + request.insert("id", m_commandCounter); + request.insert("method", method); + request.insert("params", params); + QJsonDocument jsonDoc = QJsonDocument::fromVariant(request); + + QSslSocket *socket = new QSslSocket(this); + typedef void (QSslSocket:: *sslErrorsSignal)(const QList &); + QObject::connect(socket, static_cast(&QSslSocket::sslErrors), this, &BaseTest::sslSocketSslErrors); + + QSignalSpy spyConnection(socket, &QSslSocket::connected); + socket->connectToHostEncrypted(Engine::instance()->tcpSocketServer()->serverUrl().host(), + static_cast(Engine::instance()->tcpSocketServer()->serverUrl().port())); + spyConnection.wait(); + if (spyConnection.count() == 0) { + return QVariant(); + } + + QSignalSpy dataSpy(socket, &QSslSocket::readyRead); + socket->write(jsonDoc.toJson(QJsonDocument::Compact) + '\n'); + // FIXME: check why it waits the full time here + dataSpy.wait(500); + if (dataSpy.count() != 1) { + qWarning() << "No data received"; + return QVariant(); + } + + QByteArray data = socket->readAll(); + socket->close(); + socket->deleteLater(); + + // Make sure the response ends with '}\n' + if (!data.endsWith("}\n")) { + qWarning() << "JSON data does not end with \"}\n\""; + return QVariant(); + } + + // Make sure the response it a valid JSON string + QJsonParseError error; + jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qWarning() << "JSON parser error" << error.errorString(); + return QVariant(); + } + + QVariantMap response = jsonDoc.toVariant().toMap(); + + if (response.value("id").toInt() == m_commandCounter) { + m_commandCounter++; + return jsonDoc.toVariant(); + } + + m_commandCounter++; + return QVariant(); +} + bool BaseTest::createRemoteConnection(const QString &token, const QString &nonce, QObject *parent) { // Configure mock authenticator @@ -308,20 +399,58 @@ bool BaseTest::createRemoteConnection(const QString &token, const QString &nonce return true; } + +QVariant BaseTest::injectTcpSocketData(const QByteArray &data) +{ + QSslSocket *socket = new QSslSocket(this); + typedef void (QSslSocket:: *sslErrorsSignal)(const QList &); + QObject::connect(socket, static_cast(&QSslSocket::sslErrors), this, &BaseTest::sslSocketSslErrors); + + QSignalSpy spyConnection(socket, &QSslSocket::connected); + socket->connectToHostEncrypted(Engine::instance()->tcpSocketServer()->serverUrl().host(), + static_cast(Engine::instance()->tcpSocketServer()->serverUrl().port())); + spyConnection.wait(); + if (spyConnection.count() == 0) { + return QVariant(); + } + + QSignalSpy dataSpy(socket, &QSslSocket::readyRead); + socket->write(data + '\n'); + dataSpy.wait(); + // FIXME: check why it waits the full time here + dataSpy.wait(500); + if (dataSpy.count() != 1) { + qWarning() << "No data received"; + return QVariant(); + } + + QByteArray socketData = socket->readAll(); + socket->close(); + socket->deleteLater(); + + // Make sure the response ends with '}\n' + if (!socketData.endsWith("}\n")) { + qWarning() << "JSON data does not end with \"}\n\""; + return QVariant(); + } + + // Make sure the response it a valid JSON string + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(socketData, &error); + if (error.error != QJsonParseError::NoError) { + qWarning() << "JSON parser error" << error.errorString(); + return QVariant(); + } + + m_commandCounter++; + return jsonDoc.toVariant(); +} + void BaseTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); - m_configuration = new ProxyConfiguration(this); - m_configuration->loadConfiguration(":/test-configuration.conf"); - - m_mockAuthenticator = new MockAuthenticator(this); - m_dummyAuthenticator = new DummyAuthenticator(this); - //m_awsAuthenticator = new AwsAuthenticator(m_configuration->awsCredentialsUrl(), this); - - m_authenticator = qobject_cast(m_mockAuthenticator); - m_testToken = "eyJraWQiOiJXdnFFT3prVVh5VDlINzFyRUpoNWdxRnkxNFhnR2l3SFAzVEIzUFQ1V3ZrPSIsImFsZyI6IlJT" "MjU2In0.eyJzdWIiOiJmZTJmZDNlNC1hMGJhLTQ1OTUtOWRiZS00ZDkxYjRiMjFlMzUiLCJhdWQiOiI4cmpoZ" "mRsZjlqZjFzdW9rMmpjcmx0ZDZ2IiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV2ZW50X2lkIjoiN2Y5NTRiNm" @@ -335,20 +464,13 @@ void BaseTest::initTestCase() "pQbj58v1vktaAEATdmKmlzmcix-HJK9wWHRSuv3TsNa8DGxvcPOoeTu8Vql7krZ-y7Zu-s2WsgeP4VxyT80VE" "T_xh6pMkOhE6g"; - qCDebug(dcApplication()) << "Init test case."; - restartEngine(); + qCDebug(dcApplication()) << "Init test case done."; + //restartEngine(); } void BaseTest::cleanupTestCase() { qCDebug(dcApplication()) << "Clean up test case."; - delete m_configuration; - delete m_mockAuthenticator; - delete m_dummyAuthenticator; - //delete m_awsAuthenticator; - - m_authenticator = nullptr; - cleanUpEngine(); } diff --git a/tests/testbase/basetest.h b/tests/testbase/basetest.h index 09e223f..4643272 100644 --- a/tests/testbase/basetest.h +++ b/tests/testbase/basetest.h @@ -57,6 +57,7 @@ protected: ProxyConfiguration *m_configuration = nullptr; QUrl m_serverUrl = QUrl("wss://127.0.0.1:1212"); + QUrl m_serverUrlTcp = QUrl("ssl://127.0.0.1:1213"); QSslConfiguration m_sslConfiguration; @@ -78,8 +79,11 @@ protected: void startServer(); void stopServer(); - QVariant invokeApiCall(const QString &method, const QVariantMap params = QVariantMap(), bool remainsConnected = true); - QVariant injectSocketData(const QByteArray &data); + QVariant invokeWebSocketApiCall(const QString &method, const QVariantMap params = QVariantMap(), bool remainsConnected = true); + QVariant injectWebSocketData(const QByteArray &data); + + QVariant invokeTcpSocketApiCall(const QString &method, const QVariantMap params = QVariantMap(), bool remainsConnected = true); + QVariant injectTcpSocketData(const QByteArray &data); bool createRemoteConnection(const QString &token, const QString &nonce, QObject *parent); @@ -88,8 +92,13 @@ protected slots: void cleanupTestCase(); public slots: + inline void sslSocketSslErrors(const QList &) { + QSslSocket *socket = static_cast(sender()); + socket->ignoreSslErrors(); + } + inline void sslErrors(const QList &) { - QWebSocket *socket = static_cast(sender()); + QWebSocket *socket = static_cast(sender()); socket->ignoreSslErrors(); } diff --git a/tests/testbase/mockauthenticator.cpp b/tests/testbase/mockauthenticator.cpp index d699253..d66ba56 100644 --- a/tests/testbase/mockauthenticator.cpp +++ b/tests/testbase/mockauthenticator.cpp @@ -55,8 +55,7 @@ void MockAuthenticator::replyFinished() { MockAuthenticationReply *reply = static_cast(sender()); - qCDebug(dcAuthentication()) << name() << "Authentication finished."; - + qCDebug(dcAuthentication()) << name() << "Authentication finished" << reply << reply->authenticationReply(); setReplyError(reply->authenticationReply(), reply->error()); setReplyFinished(reply->authenticationReply()); reply->deleteLater(); @@ -64,7 +63,7 @@ void MockAuthenticator::replyFinished() AuthenticationReply *MockAuthenticator::authenticate(ProxyClient *proxyClient) { - qCDebug(dcAuthentication()) << name() << "Start authentication for" << proxyClient << "using token" << proxyClient->token(); + qCDebug(dcAuthentication()) << name() << "Start authentication for" << proxyClient << "using token" << proxyClient->token() << "Auth duration" << m_timeoutDuration << "[ms]"; AuthenticationReply *authenticationReply = createAuthenticationReply(proxyClient, proxyClient); @@ -84,3 +83,8 @@ MockAuthenticationReply::MockAuthenticationReply(int timeout, Authenticator::Aut QTimer::singleShot(timeout, this, &MockAuthenticationReply::finished); } +MockAuthenticationReply::~MockAuthenticationReply() +{ + +} + diff --git a/tests/testbase/mockauthenticator.h b/tests/testbase/mockauthenticator.h index fe80b83..77bae35 100644 --- a/tests/testbase/mockauthenticator.h +++ b/tests/testbase/mockauthenticator.h @@ -40,6 +40,7 @@ class MockAuthenticationReply : public QObject Q_OBJECT public: explicit MockAuthenticationReply(int timeout, Authenticator::AuthenticationError error, AuthenticationReply *authenticationReply, QObject *parent = nullptr); + ~MockAuthenticationReply(); AuthenticationReply *authenticationReply() const { return m_authenticationReply; } Authenticator::AuthenticationError error() const { return m_error; } @@ -66,7 +67,7 @@ public: void setExpectedAuthenticationError(Authenticator::AuthenticationError error = AuthenticationErrorNoError); private: - int m_timeoutDuration = 1000; + int m_timeoutDuration = 500; Authenticator::AuthenticationError m_expectedError; private slots: