From 30d6a9dd4373056d4a7e7bab5ade9a1a9860ff20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Tue, 3 Aug 2021 19:52:00 +0200 Subject: [PATCH] First working connection mechanism for tunnel proxy --- common/common.pri | 7 + common/slipdataprocessor.cpp | 108 +++++++ common/slipdataprocessor.h | 64 ++++ docs/remote-connection-basic-flow.png | Bin 106255 -> 104547 bytes docs/remote-connection-basic-flow.svg | 2 +- docs/remote-connection-basic-flow.txt | 17 +- libnymea-remoteproxy/jsonrpc/jsonhandler.cpp | 6 +- .../jsonrpc/tunnelproxyhandler.cpp | 36 ++- .../jsonrpc/tunnelproxyhandler.h | 2 +- libnymea-remoteproxy/libnymea-remoteproxy.pro | 1 + libnymea-remoteproxy/proxy/proxyclient.cpp | 16 +- libnymea-remoteproxy/server/jsonrpcserver.cpp | 53 +++- libnymea-remoteproxy/server/jsonrpcserver.h | 3 +- .../server/tcpsocketserver.cpp | 1 - .../server/transportclient.cpp | 38 ++- libnymea-remoteproxy/server/transportclient.h | 20 +- .../tunnelproxy/tunnelproxyclient.cpp | 47 +-- .../tunnelproxy/tunnelproxyclient.h | 4 - .../tunnelproxyclientconnection.cpp | 10 + .../tunnelproxy/tunnelproxyclientconnection.h | 5 +- .../tunnelproxy/tunnelproxyserver.cpp | 112 ++++++- .../tunnelproxy/tunnelproxyserver.h | 6 +- .../tunnelproxyserverconnection.cpp | 30 ++ .../tunnelproxy/tunnelproxyserverconnection.h | 10 + .../libnymea-remoteproxyclient.pro | 1 + .../proxyjsonrpcclient.cpp | 27 +- .../proxyjsonrpcclient.h | 4 + .../tunnelproxyremoteconnection.cpp | 296 +++++++++++++++++- .../tunnelproxy/tunnelproxyremoteconnection.h | 125 +++++++- .../tunnelproxy/tunnelproxysocket.cpp | 53 +++- .../tunnelproxy/tunnelproxysocket.h | 30 +- .../tunnelproxy/tunnelproxysocketserver.cpp | 126 +++++++- .../tunnelproxy/tunnelproxysocketserver.h | 40 ++- nymea-remoteproxy.pro | 14 +- .../remoteproxyteststunnelproxy.cpp | 135 ++++++-- .../remoteproxyteststunnelproxy.h | 1 + tests/testbase/basetest.cpp | 110 ++++++- tests/testbase/basetest.h | 4 +- tests/testbase/testbase.pri | 2 + 39 files changed, 1425 insertions(+), 141 deletions(-) create mode 100644 common/common.pri create mode 100644 common/slipdataprocessor.cpp create mode 100644 common/slipdataprocessor.h diff --git a/common/common.pri b/common/common.pri new file mode 100644 index 0000000..f1eddb5 --- /dev/null +++ b/common/common.pri @@ -0,0 +1,7 @@ +INCLUDEPATH += $$PWD + +HEADERS += \ + $$PWD/slipdataprocessor.h + +SOURCES += \ + $$PWD/slipdataprocessor.cpp diff --git a/common/slipdataprocessor.cpp b/common/slipdataprocessor.cpp new file mode 100644 index 0000000..c02709d --- /dev/null +++ b/common/slipdataprocessor.cpp @@ -0,0 +1,108 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, 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 Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser 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 "slipdataprocessor.h" + +#include + +QByteArray SlipDataProcessor::deserializeData(const QByteArray &data) +{ + QByteArray deserializedData; + // Parse serial data + bool escaped = false; + for (int i = 0; i < data.length(); i++) { + quint8 byte = static_cast(data.at(i)); + + if (escaped) { + if (byte == ProtocolByteTransposedEnd) { + deserializedData.append(static_cast(ProtocolByteEnd)); + } else if (byte == ProtocolByteTransposedEsc) { + deserializedData.append(static_cast(ProtocolByteEsc)); + } else { + qWarning() << "Error while deserializing data. Escape character received but the escaped character was not recognized."; + return QByteArray(); + } + + escaped = false; + continue; + } + + // If escape byte, the next byte has to be a modified byte + if (byte == ProtocolByteEsc) { + escaped = true; + } else { + deserializedData.append(static_cast(byte)); + } + } + + return deserializedData; +} + +QByteArray SlipDataProcessor::serializeData(const QByteArray &data) +{ + QByteArray serializedData; + QDataStream stream(&serializedData, QIODevice::WriteOnly); +// stream << static_cast(ProtocolByteEnd); + + for (int i = 0; i < data.length(); i++) { + quint8 byte = static_cast(data.at(i)); + switch (byte) { + case ProtocolByteEnd: + stream << static_cast(ProtocolByteEsc); + stream << static_cast(ProtocolByteTransposedEnd); + break; + case ProtocolByteEsc: + stream << static_cast(ProtocolByteEsc); + stream << static_cast(ProtocolByteTransposedEsc); + break; + default: + stream << byte; + break; + } + } + + // Add the protocol end byte + stream << static_cast(ProtocolByteEnd); + + return serializedData; +} + +SlipDataProcessor::Frame SlipDataProcessor::parseFrame(const QByteArray &data) +{ + Frame frame; + frame.socketAddress = static_cast(static_cast(data.at(0)) << 8 | data.at(1)); + frame.data = data.right(data.length() - 2); + return frame; +} + +QByteArray SlipDataProcessor::buildFrame(const Frame &frame) +{ + QByteArray addressData; + QDataStream stream(&addressData, QIODevice::WriteOnly); + stream << frame.socketAddress; + return addressData + frame.data; +} diff --git a/common/slipdataprocessor.h b/common/slipdataprocessor.h new file mode 100644 index 0000000..9e848f8 --- /dev/null +++ b/common/slipdataprocessor.h @@ -0,0 +1,64 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, 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 Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser 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 SLIPDATAPROCESSOR_H +#define SLIPDATAPROCESSOR_H + +#include +#include + +// SLIP: https://tools.ietf.org/html/rfc1055 + +class SlipDataProcessor +{ + Q_GADGET + +public: + enum ProtocolByte { + ProtocolByteEnd = 0xC0, + ProtocolByteEsc = 0xDB, + ProtocolByteTransposedEnd = 0xDC, + ProtocolByteTransposedEsc = 0xDD + }; + Q_ENUM(ProtocolByte) + + typedef struct Frame { + quint16 socketAddress; + QByteArray data; + } Frame; + + explicit SlipDataProcessor() = default; + + static QByteArray deserializeData(const QByteArray &data); + static QByteArray serializeData(const QByteArray &data); + + static Frame parseFrame(const QByteArray &data); + static QByteArray buildFrame(const Frame &frame); + +}; + +#endif // SLIPDATAPROCESSOR_H diff --git a/docs/remote-connection-basic-flow.png b/docs/remote-connection-basic-flow.png index 50480023bcadfc3617bd0ddf3d114c13f57b5ef5..dceb02da00e3db5e96c74a951c8cc62d9da9ae3f 100644 GIT binary patch delta 51852 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 delta 53571 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 G*8d;Q6`BPA diff --git a/docs/remote-connection-basic-flow.svg b/docs/remote-connection-basic-flow.svg index 1d5a26c..f3bf1ea 100644 --- a/docs/remote-connection-basic-flow.svg +++ b/docs/remote-connection-basic-flow.svg @@ -1 +1 @@ -title%20Remote%20tunnel%20proxy%0A%0Anymea-%3Eproxy%3A%20TunnelProxy.RegisterServer(serverUuid%2C%20serverName)%0A%0Anote%20over%20proxy%3A%20Register%20the%20server%20using%20the%20uuid%0A%0Anymea%3C-proxy%3A%20TunnelProxyErrorNoError%0A%0Anote%20over%20nymea%2C%20proxy%3A%20Protocol%20from%20now%20on%20SLIP%5CnThe%20proxy%20is%20client%200x0000%0A%0Anote%20over%20nymea%2C%20proxy%3A%20SLIP%20encoded%20data%3A%202%20Bytes%20address%20%2B%20data%0A%0Aproxy%3C-client%3A%20TunnelProxy.RegisterClient(clientUuid%2C%20clientName%2C%20serverUuid)%0A%0Anote%20over%20proxy%3A%20Search%20server%20with%20given%20uuid%0A%0Anote%20over%20proxy%3A%20Server%3A%20Assign%20address%20for%20this%20client%20socket%20(0x0001)%0A%0Aproxy-%3Enymea%3A%20SLIP%3A0x0000%3A%20ProxyTunnel.ClientConnected%20(address%3A%200x0001)%0A%0Aproxy%3C-nymea%3A%20SLIP%3A0x0000%3A%20ProxyTunnel.AcceptClient%20(address%3A%200x0001)%0A%0Aproxy-%3Enymea%3A%20TunnelProxyErrorNoError%0A%0Aproxy-%3Eclient%3A%20ProxyTunnel.ConfirmClient(uuid%2C%20name%2C%20serverUuid)%0A%0Anote%20over%20client%3A%20Connected%5CnAny%20incomming%20and%20outgoing%20data%20will%5Cnbe%20from%20the%20connected%20nymea%20instance%5Cnuntil%20disconnected.%0A%0Anote%20over%20nymea%2C%20client%3A%20Connected%3A%20The%20client%20can%20now%20communicate%20with%20nymea%20directly.%5CnThe%20proxy%20will%20not%20interpret%20any%20data%20from%20the%20client%2C%20and%20from%20the%20server%20only%5Cnthe%20transmission%20frame%20(SLIP%20%2B%202%20bytes%20of%20address)%0A%0A%0A%0Aproxy%3C-client%3A%20%22data%22%0A%0Anymea%3C-proxy%3A%20SLIP%3A0x0001%20%22data%22%0A%0Anymea-%3Eproxy%3A%20SLIP%3A0x0001%20%22data%22%0A%0Aproxy-%3Eclient%3A%20%22data%22%0ARemote tunnel proxynymeaproxyclientTunnelProxy.RegisterServer(serverUuid, serverName)Register the server using the uuidTunnelProxyErrorNoErrorProtocol from now on SLIPThe proxy is client 0x0000SLIP encoded data: 2 Bytes address + dataTunnelProxy.RegisterClient(clientUuid, clientName, serverUuid)Search server with given uuidServer: Assign address for this client socket (0x0001)SLIP:0x0000: ProxyTunnel.ClientConnected (address: 0x0001)SLIP:0x0000: ProxyTunnel.AcceptClient (address: 0x0001)TunnelProxyErrorNoErrorProxyTunnel.ConfirmClient(uuid, name, serverUuid)ConnectedAny incomming and outgoing data willbe from the connected nymea instanceuntil disconnected.Connected: The client can now communicate with nymea directly.The proxy will not interpret any data from the client, and from the server onlythe transmission frame (SLIP + 2 bytes of address)"data"SLIP:0x0001 "data"SLIP:0x0001 "data""data" \ No newline at end of file +title%20Remote%20tunnel%20proxy%0A%0Anymea-%3Eproxy%3A%20TunnelProxy.RegisterServer(serverUuid%2C%20serverName)%0A%0Anote%20over%20proxy%3A%20Register%20the%20server%20using%20the%20uuid%0A%0Anymea%3C-proxy%3A%20TunnelProxyErrorNoError%0A%0Anote%20over%20nymea%2C%20proxy%3A%20Protocol%20from%20now%20on%20SLIP%5CnThe%20proxy%20is%20client%200x0000%0A%0Anote%20over%20nymea%2C%20proxy%3A%20SLIP%20encoded%20data%3A%202%20Bytes%20address%20%2B%20data%0A%0Aproxy%3C-client%3A%20TunnelProxy.RegisterClient(clientUuid%2C%20clientName%2C%20serverUuid)%0A%0Anote%20over%20proxy%3A%20Search%20server%20with%20given%20uuid%0A%0Anote%20over%20proxy%3A%20Server%3A%20Assign%20address%20for%20this%20client%20socket%20(0x0001)%0A%0Aproxy-%3Enymea%3A%20SLIP%3A0x0000%3A%20ProxyTunnel.ClientConnected%20(address%3A%200x0001)%0A%0A%0Aproxy-%3Eclient%3A%20TunnelProxyErrorNoError%0A%0Anote%20over%20client%3A%20Connected%5CnAny%20incomming%20and%20outgoing%20data%20will%5Cnbe%20from%20the%20connected%20nymea%20instance%5Cnuntil%20disconnected.%0A%0Anote%20over%20nymea%2C%20client%3A%20Connected%3A%20The%20client%20can%20now%20communicate%20with%20nymea%20directly.%5CnThe%20proxy%20will%20not%20interpret%20any%20data%20from%20the%20client%2C%20and%20from%20the%20server%20only%5Cnthe%20transmission%20frame%20(SLIP%20%2B%202%20bytes%20of%20address)%0A%0A%0A%0Aproxy%3C-client%3A%20%22request%20data%22%0A%0Anymea%3C-proxy%3A%20SLIP%3A0x0001%20%22request%20data%22%0A%0Anymea-%3Eproxy%3A%20SLIP%3A0x0001%20%22response%20data%22%0A%0Aproxy-%3Eclient%3A%20%22response%20data%22%0A%0Anymea-%3Eproxy%3A%20SLIP%3A0x0001%20%22notification%20data%22%0A%0Aproxy-%3Eclient%3A%20%22notification%20data%22%0ARemote tunnel proxynymeaproxyclientTunnelProxy.RegisterServer(serverUuid, serverName)Register the server using the uuidTunnelProxyErrorNoErrorProtocol from now on SLIPThe proxy is client 0x0000SLIP encoded data: 2 Bytes address + dataTunnelProxy.RegisterClient(clientUuid, clientName, serverUuid)Search server with given uuidServer: Assign address for this client socket (0x0001)SLIP:0x0000: ProxyTunnel.ClientConnected (address: 0x0001)TunnelProxyErrorNoErrorConnectedAny incomming and outgoing data willbe from the connected nymea instanceuntil disconnected.Connected: The client can now communicate with nymea directly.The proxy will not interpret any data from the client, and from the server onlythe transmission frame (SLIP + 2 bytes of address)"request data"SLIP:0x0001 "request data"SLIP:0x0001 "response data""response data"SLIP:0x0001 "notification data""notification data" \ No newline at end of file diff --git a/docs/remote-connection-basic-flow.txt b/docs/remote-connection-basic-flow.txt index 29b93a8..0b80577 100644 --- a/docs/remote-connection-basic-flow.txt +++ b/docs/remote-connection-basic-flow.txt @@ -18,11 +18,8 @@ note over proxy: Server: Assign address for this client socket (0x0001) proxy->nymea: SLIP:0x0000: ProxyTunnel.ClientConnected (address: 0x0001) -proxy<-nymea: SLIP:0x0000: ProxyTunnel.AcceptClient (address: 0x0001) -proxy->nymea: TunnelProxyErrorNoError - -proxy->client: ProxyTunnel.ConfirmClient(uuid, name, serverUuid) +proxy->client: TunnelProxyErrorNoError note over client: Connected\nAny incomming and outgoing data will\nbe from the connected nymea instance\nuntil disconnected. @@ -30,10 +27,14 @@ note over nymea, client: Connected: The client can now communicate with nymea di -proxy<-client: "data" +proxy<-client: "request data" -nymea<-proxy: SLIP:0x0001 "data" +nymea<-proxy: SLIP:0x0001 "request data" -nymea->proxy: SLIP:0x0001 "data" +nymea->proxy: SLIP:0x0001 "response data" -proxy->client: "data" +proxy->client: "response data" + +nymea->proxy: SLIP:0x0001 "notification data" + +proxy->client: "notification data" diff --git a/libnymea-remoteproxy/jsonrpc/jsonhandler.cpp b/libnymea-remoteproxy/jsonrpc/jsonhandler.cpp index fc33f35..8b9dc88 100644 --- a/libnymea-remoteproxy/jsonrpc/jsonhandler.cpp +++ b/libnymea-remoteproxy/jsonrpc/jsonhandler.cpp @@ -101,7 +101,7 @@ QPair JsonHandler::validateReturns(const QString &methodName, con void JsonHandler::setDescription(const QString &methodName, const QString &description) { - for(int i = 0; i < metaObject()->methodCount(); ++i) { + for (int i = 0; i < metaObject()->methodCount(); ++i) { QMetaMethod method = metaObject()->method(i); if (method.name() == methodName) { m_descriptions.insert(methodName, description); @@ -113,7 +113,7 @@ void JsonHandler::setDescription(const QString &methodName, const QString &descr void JsonHandler::setParams(const QString &methodName, const QVariantMap ¶ms) { - for(int i = 0; i < metaObject()->methodCount(); ++i) { + for (int i = 0; i < metaObject()->methodCount(); ++i) { QMetaMethod method = metaObject()->method(i); if (method.name() == methodName) { m_params.insert(methodName, params); @@ -125,7 +125,7 @@ void JsonHandler::setParams(const QString &methodName, const QVariantMap ¶ms void JsonHandler::setReturns(const QString &methodName, const QVariantMap &returns) { - for(int i = 0; i < metaObject()->methodCount(); ++i) { + for (int i = 0; i < metaObject()->methodCount(); ++i) { QMetaMethod method = metaObject()->method(i); if (method.name() == methodName) { m_returns.insert(methodName, returns); diff --git a/libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.cpp b/libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.cpp index 277397d..c5456d8 100644 --- a/libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.cpp +++ b/libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.cpp @@ -47,11 +47,21 @@ TunnelProxyHandler::TunnelProxyHandler(QObject *parent) : JsonHandler(parent) params.insert("serverUuid", JsonTypes::basicTypeToString(JsonTypes::Uuid)); setParams("RegisterServer", params); returns.insert("tunnelProxyError", JsonTypes::tunnelProxyErrorRef()); + returns.insert("slipEnabled", JsonTypes::basicTypeToString(JsonTypes::Bool)); setReturns("RegisterServer", returns); + params.clear(); returns.clear(); + setDescription("DisconnectClient", "A registered server can ask the remote proxy connection to disconnect a client for whatever reason."); + params.insert("socketAddress", JsonTypes::basicTypeToString(JsonTypes::UInt)); + setParams("DisconnectClient", params); + returns.insert("tunnelProxyError", JsonTypes::tunnelProxyErrorRef()); + setReturns("DisconnectClient", returns); + + // Client params.clear(); returns.clear(); - setDescription("RegisterClient", "Register a new TunnelProxy client on TunnelProxy server with the given serverUuid.."); + setDescription("RegisterClient", "Register a new TunnelProxy client on TunnelProxy server with the given serverUuid. " + "On success, the remote connection has been accepted and any further data will come from the connected server."); params.insert("clientName", JsonTypes::basicTypeToString(JsonTypes::String)); params.insert("clientUuid", JsonTypes::basicTypeToString(JsonTypes::Uuid)); params.insert("serverUuid", JsonTypes::basicTypeToString(JsonTypes::Uuid)); @@ -64,10 +74,11 @@ TunnelProxyHandler::TunnelProxyHandler(QObject *parent) : JsonHandler(parent) // Server params.clear(); returns.clear(); setDescription("ClientConnected", "Emitted whenever a new client has been connected to a registered server. " - "Only tunnel proxy clients registered as server will receive this notification."); - params.insert("clientId", JsonTypes::basicTypeToString(JsonTypes::UInt)); - params.insert("name", JsonTypes::basicTypeToString(JsonTypes::String)); - params.insert("address", JsonTypes::basicTypeToString(JsonTypes::String)); + "Only tunnel proxy clients registered as server will receive this notification. The socket address will be used for framing."); + params.insert("clientName", JsonTypes::basicTypeToString(JsonTypes::String)); + params.insert("clientUuid", JsonTypes::basicTypeToString(JsonTypes::String)); + params.insert("clientPeerAddress", JsonTypes::basicTypeToString(JsonTypes::String)); + params.insert("socketAddress", JsonTypes::basicTypeToString(JsonTypes::UInt)); setParams("ClientConnected", params); params.clear(); returns.clear(); @@ -100,9 +111,21 @@ JsonReply *TunnelProxyHandler::RegisterServer(const QVariantMap ¶ms, Transpo QVariantMap response; response.insert("tunnelProxyError", JsonTypes::tunnelProxyErrorToString(error)); + response.insert("slipEnabled", error == TunnelProxyServer::TunnelProxyErrorNoError); return createReply("RegisterServer", response); } +JsonReply *TunnelProxyHandler::DisconnectClient(const QVariantMap ¶ms, TransportClient *transportClient) +{ + qCDebug(dcJsonRpc()) << name() << "disconnect client requested" << params << transportClient; + quint16 socketAddress = static_cast(params.value("socketAddress").toUInt()); + TunnelProxyServer::TunnelProxyError error = Engine::instance()->tunnelProxyServer()->disconnectClient(transportClient->clientId(), socketAddress); + + QVariantMap response; + response.insert("tunnelProxyError", JsonTypes::tunnelProxyErrorToString(error)); + return createReply("DisconnectClient", response); +} + JsonReply *TunnelProxyHandler::RegisterClient(const QVariantMap ¶ms, TransportClient *transportClient) { qCDebug(dcJsonRpc()) << name() << "register client" << params << transportClient; @@ -122,9 +145,10 @@ JsonReply *TunnelProxyHandler::RegisterClient(const QVariantMap ¶ms, Transpo QVariantMap response; response.insert("tunnelProxyError", JsonTypes::tunnelProxyErrorToString(error)); - return createReply("RegisterServer", response); + return createReply("RegisterClient", response); } + //JsonReply *TunnelProxyHandler::RemoveClient(const QVariantMap ¶ms, TransportClient *transportClient) //{ diff --git a/libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.h b/libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.h index 0265e21..4fd1e62 100644 --- a/libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.h +++ b/libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.h @@ -46,9 +46,9 @@ public: QString name() const override; Q_INVOKABLE JsonReply *RegisterServer(const QVariantMap ¶ms, TransportClient *transportClient); + Q_INVOKABLE JsonReply *DisconnectClient(const QVariantMap ¶ms, TransportClient *transportClient); Q_INVOKABLE JsonReply *RegisterClient(const QVariantMap ¶ms, TransportClient *transportClient); -// Q_INVOKABLE JsonReply *RemoveClient(const QVariantMap ¶ms, TransportClient *transportClient); signals: void ClientConnected(const QVariantMap ¶ms, TransportClient *transportClient); diff --git a/libnymea-remoteproxy/libnymea-remoteproxy.pro b/libnymea-remoteproxy/libnymea-remoteproxy.pro index 7ebe9f4..35d7905 100644 --- a/libnymea-remoteproxy/libnymea-remoteproxy.pro +++ b/libnymea-remoteproxy/libnymea-remoteproxy.pro @@ -1,4 +1,5 @@ include(../nymea-remoteproxy.pri) +include(../common/common.pri) TEMPLATE = lib TARGET = nymea-remoteproxy diff --git a/libnymea-remoteproxy/proxy/proxyclient.cpp b/libnymea-remoteproxy/proxy/proxyclient.cpp index 7f170a3..1189dd6 100644 --- a/libnymea-remoteproxy/proxy/proxyclient.cpp +++ b/libnymea-remoteproxy/proxy/proxyclient.cpp @@ -130,16 +130,16 @@ QList ProxyClient::processData(const QByteArray &data) QList packages; // Handle packet fragmentation - m_dataBuffers.append(data); - int splitIndex = m_dataBuffers.indexOf("}\n{"); + m_dataBuffer.append(data); + int splitIndex = m_dataBuffer.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{"); + packages.append(m_dataBuffer.left(splitIndex + 1)); + m_dataBuffer = m_dataBuffer.right(m_dataBuffer.length() - splitIndex - 2); + splitIndex = m_dataBuffer.indexOf("}\n{"); } - if (m_dataBuffers.trimmed().endsWith("}")) { - packages.append(m_dataBuffers); - m_dataBuffers.clear(); + if (m_dataBuffer.trimmed().endsWith("}")) { + packages.append(m_dataBuffer); + m_dataBuffer.clear(); } return packages; diff --git a/libnymea-remoteproxy/server/jsonrpcserver.cpp b/libnymea-remoteproxy/server/jsonrpcserver.cpp index c1b6c5e..8f92ee3 100644 --- a/libnymea-remoteproxy/server/jsonrpcserver.cpp +++ b/libnymea-remoteproxy/server/jsonrpcserver.cpp @@ -29,6 +29,8 @@ #include "jsonrpcserver.h" #include "loggingcategories.h" #include "jsonrpc/jsontypes.h" +#include "transportclient.h" +#include "slipdataprocessor.h" #include #include @@ -135,7 +137,14 @@ void JsonRpcServer::sendResponse(TransportClient *client, int commandId, const Q QByteArray data = QJsonDocument::fromVariant(response).toJson(QJsonDocument::Compact); qCDebug(dcJsonRpcTraffic()) << "Sending data:" << data; - client->sendData(data); + if (client->slipEnabled()) { + SlipDataProcessor::Frame frame; + frame.socketAddress = 0x0000; + frame.data = data; + client->sendData(SlipDataProcessor::serializeData(SlipDataProcessor::buildFrame(frame))); + } else { + client->sendData(data); + } } void JsonRpcServer::sendErrorResponse(TransportClient *client, int commandId, const QString &error) @@ -147,7 +156,14 @@ void JsonRpcServer::sendErrorResponse(TransportClient *client, int commandId, co QByteArray data = QJsonDocument::fromVariant(errorResponse).toJson(QJsonDocument::Compact); qCDebug(dcJsonRpcTraffic()) << "Sending data:" << data; - client->sendData(data); + if (client->slipEnabled()) { + SlipDataProcessor::Frame frame; + frame.socketAddress = 0x0000; + frame.data = data; + client->sendData(SlipDataProcessor::serializeData(SlipDataProcessor::buildFrame(frame))); + } else { + client->sendData(data); + } } QString JsonRpcServer::formatAssertion(const QString &targetNamespace, const QString &method, JsonHandler *handler, const QVariantMap &data) const @@ -239,18 +255,21 @@ void JsonRpcServer::processDataPackage(TransportClient *transportClient, const Q Q_ASSERT_X((targetNamespace == "RemoteProxy" && method == "Introspect") || handler->validateReturns(method, reply->data()).first ,"validating return value", formatAssertion(targetNamespace, method, handler, reply->data()).toLatin1().data()); -// QPair returnValidation = reply->handler()->validateReturns(reply->method(), reply->data()); -// if (!returnValidation.first) { -// qCWarning(dcJsonRpc()) << "Return value validation failed of sync reply. This should never happen. Please check the source code."; -// } - reply->setClientId(transportClient->clientId()); reply->setCommandId(commandId); sendResponse(transportClient, commandId, reply->data()); - - // TODO: check if the client should be disconnected after this response - reply->deleteLater(); + + // If the server decided to kill the connection after the response, do it now + if (transportClient->killConnectionRequested()) { + transportClient->killConnection(transportClient->killConnectionReason()); + return; + } + + // Enable SLIP from now on since requested + if (transportClient->slipAfterResponseEnabled()) { + transportClient->setSlipEnabled(true); + } } } @@ -325,7 +344,7 @@ void JsonRpcServer::processData(TransportClient *transportClient, const QByteArr if (!m_clients.contains(transportClient)) return; - qCDebug(dcJsonRpcTraffic()) << "Incoming data from" << transportClient << ": " << data; + qCDebug(dcJsonRpcTraffic()) << "Incoming data from" << transportClient << ": " << qUtf8Printable(data); // Handle package fragmentation QList packages = transportClient->processData(data); @@ -350,8 +369,16 @@ void JsonRpcServer::sendNotification(const QString &nameSpace, const QString &me notification.insert("params", params); QByteArray data = QJsonDocument::fromVariant(notification).toJson(QJsonDocument::Compact); - qCDebug(dcJsonRpcTraffic()) << "Sending notification:" << data; - transportClient->sendData(data); + if (transportClient->slipEnabled()) { + SlipDataProcessor::Frame frame; + frame.socketAddress = 0x0000; + frame.data = data; + qCDebug(dcJsonRpcTraffic()) << "Sending notification frame:" <sendData(SlipDataProcessor::serializeData(SlipDataProcessor::buildFrame(frame))); + } else { + qCDebug(dcJsonRpcTraffic()) << "Sending notification:" << data; + transportClient->sendData(data); + } } } diff --git a/libnymea-remoteproxy/server/jsonrpcserver.h b/libnymea-remoteproxy/server/jsonrpcserver.h index ca84c1b..c294084 100644 --- a/libnymea-remoteproxy/server/jsonrpcserver.h +++ b/libnymea-remoteproxy/server/jsonrpcserver.h @@ -69,12 +69,13 @@ private: QString formatAssertion(const QString &targetNamespace, const QString &method, JsonHandler *handler, const QVariantMap &data) const; - void processDataPackage(TransportClient *transportClient, const QByteArray &data); private slots: void asyncReplyFinished(); public slots: + void processDataPackage(TransportClient *transportClient, const QByteArray &data); + // Client registration for JSON RPC traffic void registerClient(TransportClient *transportClient); void unregisterClient(TransportClient *transportClient); diff --git a/libnymea-remoteproxy/server/tcpsocketserver.cpp b/libnymea-remoteproxy/server/tcpsocketserver.cpp index aef4c95..762c069 100644 --- a/libnymea-remoteproxy/server/tcpsocketserver.cpp +++ b/libnymea-remoteproxy/server/tcpsocketserver.cpp @@ -65,7 +65,6 @@ void TcpSocketServer::killClientConnection(const QUuid &clientId, const QString return; qCWarning(dcTcpSocketServer()) << "Killing client connection" << clientId.toString() << "Reason:" << killReason; - client->flush(); client->close(); } diff --git a/libnymea-remoteproxy/server/transportclient.cpp b/libnymea-remoteproxy/server/transportclient.cpp index 0e108ac..58ca3b3 100644 --- a/libnymea-remoteproxy/server/transportclient.cpp +++ b/libnymea-remoteproxy/server/transportclient.cpp @@ -61,6 +61,42 @@ QString TransportClient::creationTimeString() const return QDateTime::fromMSecsSinceEpoch(creationTime() * 1000).toString("dd.MM.yyyy hh:mm:ss"); } +void TransportClient::killConnectionAfterResponse(const QString &killConnectionReason) +{ + m_killConnectionRequested = true; + m_killConnectionReason = killConnectionReason; +} + +bool TransportClient::killConnectionRequested() const +{ + return m_killConnectionRequested; +} + +QString TransportClient::killConnectionReason() const +{ + return m_killConnectionReason; +} + +void TransportClient::enableSlipAfterResponse() +{ + m_slipAfterResponseEnabled = true; +} + +bool TransportClient::slipAfterResponseEnabled() const +{ + return m_slipAfterResponseEnabled; +} + +bool TransportClient::slipEnabled() const +{ + return m_slipEnabled; +} + +void TransportClient::setSlipEnabled(bool slipEnabled) +{ + m_slipEnabled = slipEnabled; +} + TransportInterface *TransportClient::interface() const { return m_interface; @@ -108,7 +144,7 @@ void TransportClient::addTxDataCount(int dataCount) int TransportClient::bufferSize() const { - return m_dataBuffers.size(); + return m_dataBuffer.size(); } int TransportClient::generateMessageId() diff --git a/libnymea-remoteproxy/server/transportclient.h b/libnymea-remoteproxy/server/transportclient.h index a43e746..caddee0 100644 --- a/libnymea-remoteproxy/server/transportclient.h +++ b/libnymea-remoteproxy/server/transportclient.h @@ -50,6 +50,18 @@ public: uint creationTime() const; QString creationTimeString() const; + // Schedule a disconnect after the response + void killConnectionAfterResponse(const QString &killConnectionReason); + bool killConnectionRequested() const; + QString killConnectionReason() const; + + // Schedule SLIP enable after response + void enableSlipAfterResponse(); + bool slipAfterResponseEnabled() const; + + bool slipEnabled() const; + void setSlipEnabled(bool slipEnabled); + TransportInterface *interface() const; // Properties from auth request @@ -85,7 +97,13 @@ protected: QString m_name; QUuid m_uuid; - QByteArray m_dataBuffers; + QByteArray m_dataBuffer; + + bool m_killConnectionRequested = false; + QString m_killConnectionReason; + + bool m_slipAfterResponseEnabled = false; + bool m_slipEnabled = false; // Json data information int m_messageId = 0; diff --git a/libnymea-remoteproxy/tunnelproxy/tunnelproxyclient.cpp b/libnymea-remoteproxy/tunnelproxy/tunnelproxyclient.cpp index ce46ba4..b6f7519 100644 --- a/libnymea-remoteproxy/tunnelproxy/tunnelproxyclient.cpp +++ b/libnymea-remoteproxy/tunnelproxy/tunnelproxyclient.cpp @@ -1,5 +1,7 @@ #include "tunnelproxyclient.h" +#include "loggingcategories.h" #include "server/transportinterface.h" +#include "slipdataprocessor.h" namespace remoteproxy { @@ -23,35 +25,44 @@ void TunnelProxyClient::setType(Type type) emit typeChanged(m_type); } -bool TunnelProxyClient::slipEnabled() const -{ - return m_slipEnabled; -} - -void TunnelProxyClient::setSlipEnabled(bool slipEnabled) -{ - m_slipEnabled = slipEnabled; -} - QList TunnelProxyClient::processData(const QByteArray &data) { // TODO: unescape if this data is for the json handler QList packages; + // Parse packages depending on the encoded if (m_slipEnabled) { + // Read each byte until we get END byte, then unescape the package + for (int i = 0; i < data.length(); i++) { + quint8 byte = static_cast(data.at(i)); + if (byte == SlipDataProcessor::ProtocolByteEnd) { + // If there is no data...continue since it might be a starting END byte + if (m_dataBuffer.isEmpty()) + continue; + QByteArray frame = SlipDataProcessor::deserializeData(m_dataBuffer); + if (frame.isNull()) { + qCWarning(dcTunnelProxyServerTraffic()) << "Received inconsistant SLIP encoded message. Ignoring data..."; + } else { + qCDebug(dcTunnelProxyServerTraffic()) << "Frame received"; + packages.append(m_dataBuffer); + } + } else { + m_dataBuffer.append(data.at(i)); + } + } } else { // Handle json packet fragmentation - m_dataBuffers.append(data); - int splitIndex = m_dataBuffers.indexOf("}\n{"); + m_dataBuffer.append(data); + int splitIndex = m_dataBuffer.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{"); + packages.append(m_dataBuffer.left(splitIndex + 1)); + m_dataBuffer = m_dataBuffer.right(m_dataBuffer.length() - splitIndex - 2); + splitIndex = m_dataBuffer.indexOf("}\n{"); } - if (m_dataBuffers.trimmed().endsWith("}")) { - packages.append(m_dataBuffers); - m_dataBuffers.clear(); + if (m_dataBuffer.trimmed().endsWith("}")) { + packages.append(m_dataBuffer); + m_dataBuffer.clear(); } } diff --git a/libnymea-remoteproxy/tunnelproxy/tunnelproxyclient.h b/libnymea-remoteproxy/tunnelproxy/tunnelproxyclient.h index ede50de..832be0a 100644 --- a/libnymea-remoteproxy/tunnelproxy/tunnelproxyclient.h +++ b/libnymea-remoteproxy/tunnelproxy/tunnelproxyclient.h @@ -23,9 +23,6 @@ public: Type type() const; void setType(Type type); - bool slipEnabled() const; - void setSlipEnabled(bool slipEnabled); - // Json server methods QList processData(const QByteArray &data) override; @@ -34,7 +31,6 @@ signals: private: Type m_type = TypeNone; - bool m_slipEnabled = false; }; diff --git a/libnymea-remoteproxy/tunnelproxy/tunnelproxyclientconnection.cpp b/libnymea-remoteproxy/tunnelproxy/tunnelproxyclientconnection.cpp index 3279be1..667f265 100644 --- a/libnymea-remoteproxy/tunnelproxy/tunnelproxyclientconnection.cpp +++ b/libnymea-remoteproxy/tunnelproxy/tunnelproxyclientconnection.cpp @@ -66,6 +66,16 @@ QUuid TunnelProxyClientConnection::serverUuid() const return m_serverUuid; } +quint16 TunnelProxyClientConnection::socketAddress() const +{ + return m_socketAddress; +} + +void TunnelProxyClientConnection::setSocketAddress(quint16 socketAddress) +{ + m_socketAddress = socketAddress; +} + QDebug operator<<(QDebug debug, TunnelProxyClientConnection *clientConnection) { debug.nospace() << "TunnelProxyClientConnection("; diff --git a/libnymea-remoteproxy/tunnelproxy/tunnelproxyclientconnection.h b/libnymea-remoteproxy/tunnelproxy/tunnelproxyclientconnection.h index b52b121..fbbb986 100644 --- a/libnymea-remoteproxy/tunnelproxy/tunnelproxyclientconnection.h +++ b/libnymea-remoteproxy/tunnelproxy/tunnelproxyclientconnection.h @@ -51,6 +51,9 @@ public: QString clientName() const; QUuid serverUuid() const; + quint16 socketAddress() const; + void setSocketAddress(quint16 socketAddress); + private: TransportClient *m_transportClient = nullptr; TunnelProxyServerConnection *m_serverConnection = nullptr; @@ -58,7 +61,7 @@ private: QUuid m_clientUuid; QString m_clientName; QUuid m_serverUuid; - + quint16 m_socketAddress = 0xFFFF; }; QDebug operator<<(QDebug debug, TunnelProxyClientConnection *clientConnection); diff --git a/libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.cpp b/libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.cpp index 0924ae9..e56f2af 100644 --- a/libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.cpp +++ b/libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.cpp @@ -32,6 +32,8 @@ #include "tunnelproxyserverconnection.h" #include "tunnelproxyclientconnection.h" +#include "../common/slipdataprocessor.h" + namespace remoteproxy { TunnelProxyServer::TunnelProxyServer(QObject *parent) : @@ -86,12 +88,14 @@ TunnelProxyServer::TunnelProxyError TunnelProxyServer::registerServer(const QUui TunnelProxyClient *tunnelProxyClient = m_proxyClients.value(clientId); if (!tunnelProxyClient) { qCWarning(dcTunnelProxyServer()) << "There is no client with client uuid" << clientId.toString(); + tunnelProxyClient->killConnectionAfterResponse("Internal server error"); return TunnelProxyServer::TunnelProxyErrorInternalServerError; } // Make sure this client has not been registered as client or re-registration has been called... if (tunnelProxyClient->type() != TunnelProxyClient::TypeNone) { qCWarning(dcTunnelProxyServer()) << "Client tried to register as server but has already been registerd as" << tunnelProxyClient->type(); + tunnelProxyClient->killConnectionAfterResponse("Already registered"); return TunnelProxyServer::TunnelProxyErrorAlreadyRegistered; } @@ -100,7 +104,7 @@ TunnelProxyServer::TunnelProxyError TunnelProxyServer::registerServer(const QUui tunnelProxyClient->setName(serverName); // Enable SLIP from now on -// tunnelProxyClient->setSlipEnabled(true); + tunnelProxyClient->enableSlipAfterResponse(); TunnelProxyServerConnection *serverConnection = new TunnelProxyServerConnection(tunnelProxyClient, serverUuid, serverName, this); m_tunnelProxyServerConnections.insert(serverUuid, serverConnection); @@ -110,22 +114,23 @@ TunnelProxyServer::TunnelProxyError TunnelProxyServer::registerServer(const QUui TunnelProxyServer::TunnelProxyError TunnelProxyServer::registerClient(const QUuid &clientId, const QUuid &clientUuid, const QString &clientName, const QUuid &serverUuid) { - qCDebug(dcTunnelProxyServer()) << "Register new client" << m_proxyClients.value(clientId) << clientName << clientUuid.toString() << "--> server" << serverUuid.toString(); - TunnelProxyClient *tunnelProxyClient = m_proxyClients.value(clientId); if (!tunnelProxyClient) { qCWarning(dcTunnelProxyServer()) << "There is no client with client uuid" << clientId.toString(); + tunnelProxyClient->killConnectionAfterResponse("Internal server error"); return TunnelProxyServer::TunnelProxyErrorInternalServerError; } // Make sure this client has not been registered as client or re-registration has been called... if (tunnelProxyClient->type() != TunnelProxyClient::TypeNone) { qCWarning(dcTunnelProxyServer()) << "Client tried to register as client but has already been registerd as" << tunnelProxyClient->type(); + tunnelProxyClient->killConnectionAfterResponse("Already registered"); return TunnelProxyServer::TunnelProxyErrorAlreadyRegistered; } if (m_tunnelProxyClientConnections.contains(clientUuid)) { qCWarning(dcTunnelProxyServer()) << "There is a client already registered with client uuid" << clientUuid.toString(); + tunnelProxyClient->killConnectionAfterResponse("Already registered"); return TunnelProxyServer::TunnelProxyErrorAlreadyRegistered; } @@ -133,6 +138,7 @@ TunnelProxyServer::TunnelProxyError TunnelProxyServer::registerClient(const QUui TunnelProxyServerConnection *serverConnection = m_tunnelProxyServerConnections.value(serverUuid); if (!serverConnection) { qCWarning(dcTunnelProxyServer()) << "There is no server registered with server uuid" << serverUuid.toString(); + tunnelProxyClient->killConnectionAfterResponse("Unknown server"); return TunnelProxyServer::TunnelProxyErrorServerNotFound; } @@ -147,8 +153,47 @@ TunnelProxyServer::TunnelProxyError TunnelProxyServer::registerClient(const QUui qCDebug(dcTunnelProxyServer()) << "Register client" << clientConnection << "-->" << serverConnection; serverConnection->registerClientConnection(clientConnection); - // TODO: wait for te aprovement from the server + // Tell the server a new client want's to connect + QVariantMap params; + params.insert("clientName", tunnelProxyClient->name()); + params.insert("clientUuid", tunnelProxyClient->uuid().toString()); + params.insert("clientPeerAddress", tunnelProxyClient->peerAddress().toString()); + params.insert("socketAddress", clientConnection->socketAddress()); + m_jsonRpcServer->sendNotification("TunnelProxy", "ClientConnected", params, serverConnection->transportClient()); + // Note: check if a confirmation from the server would be needed, for rejection or limit or something. For now they are directly connected. + + return TunnelProxyServer::TunnelProxyErrorNoError; +} + +TunnelProxyServer::TunnelProxyError TunnelProxyServer::disconnectClient(const QUuid &clientId, quint16 socketAddress) +{ + TunnelProxyClient *tunnelProxyClient = m_proxyClients.value(clientId); + if (!tunnelProxyClient) { + qCWarning(dcTunnelProxyServer()) << "There is no client with client uuid" << clientId.toString(); + return TunnelProxyServer::TunnelProxyErrorInternalServerError; + } + + if (tunnelProxyClient->type() != TunnelProxyClient::TypeServer) { + qCWarning(dcTunnelProxyServer()) << "Client tried to disconnect a client but has not been registerd as server" << tunnelProxyClient->type(); + tunnelProxyClient->killConnectionAfterResponse("Already registered"); + return TunnelProxyServer::TunnelProxyErrorAlreadyRegistered; + } + + TunnelProxyServerConnection *serverConnection = m_tunnelProxyServerConnections.value(tunnelProxyClient->uuid()); + if (!serverConnection) { + qCWarning(dcTunnelProxyServer()) << "Could not find server connection for" << tunnelProxyClient; + tunnelProxyClient->killConnectionAfterResponse("Internal server error"); + return TunnelProxyServer::TunnelProxyErrorInternalServerError; + } + + TunnelProxyClientConnection *clientConnection = serverConnection->getClientConnection(socketAddress); + if (!clientConnection) { + qCWarning(dcTunnelProxyServer()) << "Could not find client connection for socket address" << socketAddress << "on" << serverConnection; + return TunnelProxyServer::TunnelProxyErrorUnknownSocketAddress; + } + + clientConnection->transportClient()->killConnection("Server requested disconnect."); return TunnelProxyServer::TunnelProxyErrorNoError; } @@ -202,8 +247,11 @@ void TunnelProxyServer::onClientDisconnected(const QUuid &clientId) qCWarning(dcTunnelProxyServer()) << "Could not find server connection for disconnected tunnel proxy client claiming to be a server."; } else { - - // TODO: kill all related clients + foreach (TunnelProxyClientConnection *clientConnection, serverConnection->clientConnections()) { + serverConnection->unregisterClientConnection(clientConnection); + clientConnection->setSocketAddress(0xFFFF); + clientConnection->transportClient()->killConnection("Server disconnected"); + } serverConnection->deleteLater(); } @@ -215,8 +263,10 @@ void TunnelProxyServer::onClientDisconnected(const QUuid &clientId) qCWarning(dcTunnelProxyServer()) << "Could not find client connection for disconnected tunnel proxy client claiming to be a client."; } else { if (clientConnection->serverConnection()) { + QVariantMap params; + params.insert("socketAddress", clientConnection->socketAddress()); + m_jsonRpcServer->sendNotification("TunnelProxy", "ClientDisconnected", params, clientConnection->serverConnection()->transportClient()); clientConnection->serverConnection()->unregisterClientConnection(clientConnection); - // TODO: Send client disconnected to the server } clientConnection->deleteLater(); @@ -239,23 +289,63 @@ void TunnelProxyServer::onClientDataAvailable(const QUuid &clientId, const QByte } qCDebug(dcTunnelProxyServerTraffic()) << "Client data available" << tunnelProxyClient << qUtf8Printable(data); + if (tunnelProxyClient->type() == TunnelProxyClient::TypeClient) { - // Send the data to the server using slip + frame + // Send the data to the server using slip encoded frame + TunnelProxyClientConnection *clientConnection = m_tunnelProxyClientConnections.value(tunnelProxyClient->uuid()); + if (!clientConnection) { + qCWarning(dcTunnelProxyServer()) << "Could not find a client connection for client uuid" << tunnelProxyClient->uuid().toString(); + // FIXME: check what do with this client + return; + } + Q_ASSERT_X(clientConnection->serverConnection(), "TunnelProxyServer", "The client has not been registered to a server connection"); + SlipDataProcessor::Frame frame; + frame.socketAddress = clientConnection->socketAddress(); + frame.data = data; + qCDebug(dcTunnelProxyServerTraffic()) << "Write client data to server using socket address" << clientConnection->socketAddress(); + clientConnection->serverConnection()->transportClient()->sendData(SlipDataProcessor::serializeData(SlipDataProcessor::buildFrame(frame))); } else if (tunnelProxyClient->type() == TunnelProxyClient::TypeServer) { + // Data coming from a connected server connection if (tunnelProxyClient->slipEnabled()) { + // Unpack SLIP data, get address, pipe to client or give it to the json rpc server if address 0x0000 + // Handle package fragmentation + QList frames = tunnelProxyClient->processData(data); + foreach (const QByteArray &frameData, frames) { + SlipDataProcessor::Frame frame = SlipDataProcessor::parseFrame(frameData); + if (frame.socketAddress == 0x0000) { + qCDebug(dcTunnelProxyServerTraffic()) << "Received frame for the JSON server" << tunnelProxyClient; + m_jsonRpcServer->processDataPackage(tunnelProxyClient, frame.data); + } else { + // This data seems to be for a client with the given address + TunnelProxyServerConnection *serverConnection = m_tunnelProxyServerConnections.value(tunnelProxyClient->uuid()); + if (!serverConnection) { + qCWarning(dcTunnelProxyServer()) << "Could not find server connection for" << tunnelProxyClient; + continue; + } + + // Get the client and send the data to the client + TunnelProxyClientConnection *clientConnection = serverConnection->getClientConnection(frame.socketAddress); + if (!clientConnection) { + qCWarning(dcTunnelProxyServer()) << "The server connection wants to send data to a client connection which has not been registered to the server."; + // FIXME: tell the server this client does not exist + return; + } + + qCDebug(dcTunnelProxyServerTraffic()) << "Sending data to" << clientConnection << qUtf8Printable(data); + clientConnection->transportClient()->sendData(frame.data); + } + } } else { m_jsonRpcServer->processData(tunnelProxyClient, data); } - // Unpack SLIP data, get address, pipe to client or give it to the json rpc server if address 0x0000 } else { - // Not registered yet or doing other stuff...let the JSON RPC handle this data + // Not registered yet or doing other stuff...let the JSON RPC server handle this data m_jsonRpcServer->processData(tunnelProxyClient, data); } - } } diff --git a/libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.h b/libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.h index 585d229..937b370 100644 --- a/libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.h +++ b/libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.h @@ -48,7 +48,10 @@ public: TunnelProxyErrorInvalidUuid, TunnelProxyErrorInternalServerError, TunnelProxyErrorServerNotFound, - TunnelProxyErrorAlreadyRegistered + TunnelProxyErrorForbiddenCall, + TunnelProxyErrorAlreadyRegistered, + TunnelProxyErrorNotRegistered, + TunnelProxyErrorUnknownSocketAddress }; Q_ENUM(TunnelProxyError) @@ -62,6 +65,7 @@ public: TunnelProxyServer::TunnelProxyError registerServer(const QUuid &clientId, const QUuid &serverUuid, const QString &serverName); TunnelProxyServer::TunnelProxyError registerClient(const QUuid &clientId, const QUuid &clientUuid, const QString &clientName, const QUuid &serverUuid); + TunnelProxyServer::TunnelProxyError disconnectClient(const QUuid &clientId, quint16 socketAddress); public slots: void startServer(); diff --git a/libnymea-remoteproxy/tunnelproxy/tunnelproxyserverconnection.cpp b/libnymea-remoteproxy/tunnelproxy/tunnelproxyserverconnection.cpp index 4451977..83b4dd8 100644 --- a/libnymea-remoteproxy/tunnelproxy/tunnelproxyserverconnection.cpp +++ b/libnymea-remoteproxy/tunnelproxy/tunnelproxyserverconnection.cpp @@ -55,14 +55,44 @@ QString TunnelProxyServerConnection::serverName() const return m_serverName; } +QList TunnelProxyServerConnection::clientConnections() const +{ + return m_clientConnections.values(); +} + void TunnelProxyServerConnection::registerClientConnection(TunnelProxyClientConnection *clientConnection) { m_clientConnections.insert(clientConnection->clientUuid(), clientConnection); + quint16 socketAddress = getFreeAddress(); + clientConnection->setSocketAddress(socketAddress); + m_clientConnectionsAddresses.insert(socketAddress, clientConnection); } void TunnelProxyServerConnection::unregisterClientConnection(TunnelProxyClientConnection *clientConnection) { m_clientConnections.remove(clientConnection->clientUuid()); + m_clientConnectionsAddresses.remove(clientConnection->socketAddress()); +} + +TunnelProxyClientConnection *TunnelProxyServerConnection::getClientConnection(quint16 socketAddress) +{ + return m_clientConnectionsAddresses.value(socketAddress); +} + +quint16 TunnelProxyServerConnection::getFreeAddress() +{ + m_currentAddressCounter += 1; + quint16 address = m_currentAddressCounter; + for (int i = 0; i < m_connectionLimit; i++) { + if (m_clientConnectionsAddresses.contains(address) || address == 0x0000 || address == 0xFFFF) { + address++; + } else { + break; + } + } + + Q_ASSERT_X(!m_clientConnectionsAddresses.contains(address), "TunnelProxyServerConnection", "no free address but the maximum of connections has been reached."); + return address; } QDebug operator<<(QDebug debug, TunnelProxyServerConnection *serverConnection) diff --git a/libnymea-remoteproxy/tunnelproxy/tunnelproxyserverconnection.h b/libnymea-remoteproxy/tunnelproxy/tunnelproxyserverconnection.h index 3fdaf71..471eb5a 100644 --- a/libnymea-remoteproxy/tunnelproxy/tunnelproxyserverconnection.h +++ b/libnymea-remoteproxy/tunnelproxy/tunnelproxyserverconnection.h @@ -48,17 +48,27 @@ public: QUuid serverUuid() const; QString serverName() const; + QList clientConnections() const; + void registerClientConnection(TunnelProxyClientConnection *clientConnection); void unregisterClientConnection(TunnelProxyClientConnection *clientConnection); + TunnelProxyClientConnection *getClientConnection(quint16 socketAddress); + signals: private: TransportClient *m_transportClient = nullptr; QUuid m_serverUuid; QString m_serverName; + quint16 m_connectionLimit = 100; + + quint16 m_currentAddressCounter = 0; QHash m_clientConnections; + QHash m_clientConnectionsAddresses; + + quint16 getFreeAddress(); }; diff --git a/libnymea-remoteproxyclient/libnymea-remoteproxyclient.pro b/libnymea-remoteproxyclient/libnymea-remoteproxyclient.pro index 0541c0f..3c500d6 100644 --- a/libnymea-remoteproxyclient/libnymea-remoteproxyclient.pro +++ b/libnymea-remoteproxyclient/libnymea-remoteproxyclient.pro @@ -4,6 +4,7 @@ TEMPLATE = lib TARGET = nymea-remoteproxyclient target.path = $$[QT_INSTALL_LIBS] +include(../common/common.pri) include(libnymea-remoteproxyclient.pri) installheaders.files = remoteproxyconnection.h diff --git a/libnymea-remoteproxyclient/proxyjsonrpcclient.cpp b/libnymea-remoteproxyclient/proxyjsonrpcclient.cpp index c2e04db..be9f7da 100644 --- a/libnymea-remoteproxyclient/proxyjsonrpcclient.cpp +++ b/libnymea-remoteproxyclient/proxyjsonrpcclient.cpp @@ -79,6 +79,20 @@ JsonReply *JsonRpcClient::callRegisterServer(const QUuid &serverUuid, const QStr return reply; } +JsonReply *JsonRpcClient::callRegisterClient(const QUuid &clientUuid, const QString &clientName, const QUuid &serverUuid) +{ + QVariantMap params; + params.insert("clientUuid", clientUuid); + params.insert("clientName", clientName); + params.insert("serverUuid", serverUuid.toString()); + + JsonReply *reply = new JsonReply(m_commandId, "TunnelProxy", "RegisterClient", params, this); + qCDebug(dcRemoteProxyClientJsonRpc()) << "Calling" << QString("%1.%2").arg(reply->nameSpace()).arg(reply->method()); + sendRequest(reply->requestMap()); + m_replies.insert(m_commandId, reply); + return reply; +} + void JsonRpcClient::sendRequest(const QVariantMap &request) { QByteArray data = QJsonDocument::fromVariant(request).toJson(QJsonDocument::Compact); @@ -97,6 +111,8 @@ void JsonRpcClient::processDataPackage(const QByteArray &data) QVariantMap dataMap = jsonDoc.toVariant().toMap(); + qCDebug(dcRemoteProxyClientJsonRpcTraffic()) << "Data received" << qUtf8Printable(data); + // check if this is a reply to a request int commandId = dataMap.value("id").toInt(); JsonReply *reply = m_replies.take(commandId); @@ -109,7 +125,7 @@ void JsonRpcClient::processDataPackage(const QByteArray &data) } reply->setResponse(dataMap); - reply->finished(); + emit reply->finished(); return; } @@ -126,6 +142,15 @@ void JsonRpcClient::processDataPackage(const QByteArray &data) QString clientName = notificationParams.value("name").toString(); QString clientUuid = notificationParams.value("uuid").toString(); emit tunnelEstablished(clientName, clientUuid); + } else if (nameSpace == "TunnelProxy" && notificationName == "ClientConnected") { + QString clientName = notificationParams.value("clientName").toString(); + QUuid clientUuid = notificationParams.value("clientUuid").toUuid(); + QString clientPeerAddress = notificationParams.value("clientPeerAddress").toString(); + quint16 socketAddress = static_cast(notificationParams.value("socketAddress").toUInt()); + emit tunnelProxyClientConnected(clientName, clientUuid, clientPeerAddress, socketAddress); + } else if (nameSpace == "TunnelProxy" && notificationName == "ClientDisconnected") { + quint16 socketAddress = static_cast(notificationParams.value("socketAddress").toUInt()); + emit tunnelProxyClientDisonnected(socketAddress); } } } diff --git a/libnymea-remoteproxyclient/proxyjsonrpcclient.h b/libnymea-remoteproxyclient/proxyjsonrpcclient.h index b3cb0bf..fb44b36 100644 --- a/libnymea-remoteproxyclient/proxyjsonrpcclient.h +++ b/libnymea-remoteproxyclient/proxyjsonrpcclient.h @@ -55,6 +55,8 @@ public: // Tunnel proxy JsonReply *callRegisterServer(const QUuid &serverUuid, const QString &serverName); + JsonReply *callRegisterClient(const QUuid &clientUuid, const QString &clientName, const QUuid &serverUuid); + JsonReply *callDisconnectClient(quint16 socketAddress); private: ProxyConnection *m_connection = nullptr; @@ -69,6 +71,8 @@ private: signals: void tunnelEstablished(const QString clientName, const QString &clientUuid); + void tunnelProxyClientConnected(const QString &clientName, const QUuid &clientUuid, const QString &clientPeerAddress, quint16 socketAddress); + void tunnelProxyClientDisonnected(quint16 socketAddress); public slots: void processData(const QByteArray &data); diff --git a/libnymea-remoteproxyclient/tunnelproxy/tunnelproxyremoteconnection.cpp b/libnymea-remoteproxyclient/tunnelproxy/tunnelproxyremoteconnection.cpp index 7a2e44b..cb0b0dd 100644 --- a/libnymea-remoteproxyclient/tunnelproxy/tunnelproxyremoteconnection.cpp +++ b/libnymea-remoteproxyclient/tunnelproxy/tunnelproxyremoteconnection.cpp @@ -1,6 +1,298 @@ -#include "tunnelproxyremoteconnection.h" +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, 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 Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -TunnelProxyRemoteConnection::TunnelProxyRemoteConnection(QObject *parent) : QObject(parent) +#include "tunnelproxyremoteconnection.h" +#include "proxyconnection.h" +#include "tcpsocketconnection.h" +#include "websocketconnection.h" +#include "proxyjsonrpcclient.h" + +Q_LOGGING_CATEGORY(dcTunnelProxyRemoteConnection, "TunnelProxyRemoteConnection") + +namespace remoteproxyclient { + +TunnelProxyRemoteConnection::TunnelProxyRemoteConnection(const QUuid &clientUuid, const QString &clientName, QObject *parent) : + QObject(parent), + m_clientUuid(clientUuid), + m_clientName(clientName) { } + +TunnelProxyRemoteConnection::TunnelProxyRemoteConnection(const QUuid &clientUuid, const QString &clientName, ConnectionType connectionType, QObject *parent) : + QObject(parent), + m_clientUuid(clientUuid), + m_clientName(clientName), + m_connectionType(connectionType) +{ + +} + +TunnelProxyRemoteConnection::~TunnelProxyRemoteConnection() +{ + +} + +bool TunnelProxyRemoteConnection::remoteConnected() const +{ + return m_remoteConnected; +} + +QAbstractSocket::SocketError TunnelProxyRemoteConnection::error() const +{ + return m_error; +} + +void TunnelProxyRemoteConnection::ignoreSslErrors() +{ + m_connection->ignoreSslErrors(); +} + +void TunnelProxyRemoteConnection::ignoreSslErrors(const QList &errors) +{ + m_connection->ignoreSslErrors(errors); +} + +QString TunnelProxyRemoteConnection::remoteProxyServer() const +{ + return m_remoteProxyServer; +} + +QString TunnelProxyRemoteConnection::remoteProxyServerName() const +{ + return m_remoteProxyServerName; +} + +QString TunnelProxyRemoteConnection::remoteProxyServerVersion() const +{ + return m_remoteProxyServerVersion; +} + +QString TunnelProxyRemoteConnection::remoteProxyApiVersion() const +{ + return m_remoteProxyApiVersion; +} + +bool TunnelProxyRemoteConnection::connectServer(const QUrl &url, const QUuid &serverUuid) +{ + m_serverUrl = url; + m_serverUuid = serverUuid; + m_error = QAbstractSocket::UnknownSocketError; + + cleanUp(); + + switch (m_connectionType) { + case ConnectionTypeWebSocket: + qCDebug(dcTunnelProxyRemoteConnection()) << "Creating a web socket connection to" << url.toString(); + m_connection = qobject_cast(new WebSocketConnection(this)); + break; + case ConnectionTypeTcpSocket: + qCDebug(dcTunnelProxyRemoteConnection()) << "Creating a TCP socket connection to" << url.toString(); + m_connection = qobject_cast(new TcpSocketConnection(this)); + break; + } + + connect(m_connection, &ProxyConnection::connectedChanged, this, &TunnelProxyRemoteConnection::onConnectionChanged); + connect(m_connection, &ProxyConnection::dataReceived, this, &TunnelProxyRemoteConnection::onConnectionDataAvailable); + connect(m_connection, &ProxyConnection::errorOccured, this, &TunnelProxyRemoteConnection::onConnectionSocketError); + connect(m_connection, &ProxyConnection::stateChanged, this, &TunnelProxyRemoteConnection::onConnectionStateChanged); + connect(m_connection, &ProxyConnection::sslErrors, this, &TunnelProxyRemoteConnection::sslErrors); + + m_jsonClient = new JsonRpcClient(m_connection, this); + + qCDebug(dcTunnelProxyRemoteConnection()) << "Connecting to" << m_serverUrl.toString(); + m_connection->connectServer(m_serverUrl); + + return true; +} + +void TunnelProxyRemoteConnection::disconnectServer() +{ + if (m_connection) { + qCDebug(dcTunnelProxyRemoteConnection()) << "Disconnecting from" << m_connection->serverUrl().toString(); + m_connection->disconnectServer(); + } +} + +void TunnelProxyRemoteConnection::onConnectionChanged(bool connected) +{ + if (connected) { + qCDebug(dcTunnelProxyRemoteConnection()) << "Connected to remote proxy server."; + setState(StateConnected); + setState(StateInitializing); + JsonReply *reply = m_jsonClient->callHello(); + connect(reply, &JsonReply::finished, this, &TunnelProxyRemoteConnection::onHelloFinished); + } else { + qCDebug(dcTunnelProxyRemoteConnection()) << "Disconnected from remote proxy server."; + setState(StateDisconnected); + cleanUp(); + } +} + +void TunnelProxyRemoteConnection::onConnectionDataAvailable(const QByteArray &data) +{ + if (m_state != StateRemoteConnected) { + m_jsonClient->processData(data); + return; + } + + emit dataReady(data); +} + +void TunnelProxyRemoteConnection::onConnectionSocketError(QAbstractSocket::SocketError error) +{ + setError(error); +} + +void TunnelProxyRemoteConnection::onConnectionStateChanged(QAbstractSocket::SocketState state) +{ + // Process the actuall socket state + switch (state) { + case QAbstractSocket::UnconnectedState: + setState(StateDisconnected); + break; + case QAbstractSocket::HostLookupState: + setState(StateHostLookup); + break; + case QAbstractSocket::ConnectingState: + setState(StateConnecting); + break; + case QAbstractSocket::ConnectedState: + setState(StateConnected); + break; + case QAbstractSocket::ClosingState: + setState(StateDiconnecting); + break; + case QAbstractSocket::BoundState: + case QAbstractSocket::ListeningState: + break; + } +} + +void TunnelProxyRemoteConnection::onHelloFinished() +{ + JsonReply *reply = static_cast(sender()); + reply->deleteLater(); + + QVariantMap response = reply->response(); + qCDebug(dcTunnelProxyRemoteConnection()) << "Hello response ready" << reply->commandId() << response; + + if (response.value("status").toString() != "success") { + qCWarning(dcTunnelProxyRemoteConnection()) << "Could not get initial information from proxy server."; + m_connection->disconnectServer(); + return; + } + + QVariantMap responseParams = response.value("params").toMap(); + m_remoteProxyServer = responseParams.value("server").toString(); + m_remoteProxyServerName = responseParams.value("name").toString(); + m_remoteProxyServerVersion = responseParams.value("version").toString(); + m_remoteProxyApiVersion = responseParams.value("apiVersion").toString(); + + setState(StateRegister); + + JsonReply *registerReply = m_jsonClient->callRegisterClient(m_clientUuid, m_clientName, m_serverUuid); + connect(registerReply, &JsonReply::finished, this, &TunnelProxyRemoteConnection::onClientRegistrationFinished); +} + +void TunnelProxyRemoteConnection::onClientRegistrationFinished() +{ + JsonReply *reply = static_cast(sender()); + reply->deleteLater(); + + QVariantMap response = reply->response(); + qCDebug(dcTunnelProxyRemoteConnection()) << "RegisterClient response ready" << reply->commandId() << response; + + if (response.value("status").toString() != "success") { + qCWarning(dcTunnelProxyRemoteConnection()) << "JSON RPC error. Failed to register as tunnel client on the remote proxy server."; + m_connection->disconnectServer(); + return; + } + + QVariantMap responseParams = response.value("params").toMap(); + if (responseParams.value("tunnelProxyError").toString() != "TunnelProxyErrorNoError") { + qCWarning(dcTunnelProxyRemoteConnection()) << "Failed to register as tunnel client on the remote proxy server:" << responseParams.value("tunnelProxyError").toString(); + m_connection->disconnectServer(); + return; + } + + qCDebug(dcTunnelProxyRemoteConnection()) << "Registered successfully as tunnel client on the remote proxy server."; + setState(StateRemoteConnected); +} + +void TunnelProxyRemoteConnection::setState(State state) +{ + if (m_state == state) + return; + + m_state = state; + qCDebug(dcTunnelProxyRemoteConnection()) << "State changed" << m_state; + emit stateChanged(m_state); + + setRemoteConnected(m_state == StateRemoteConnected); +} + +void TunnelProxyRemoteConnection::setRemoteConnected(bool remoteConnected) +{ + if (m_remoteConnected == remoteConnected) + return; + + m_remoteConnected = remoteConnected; + emit remoteConnectedChanged(m_remoteConnected); +} + +void TunnelProxyRemoteConnection::setError(QAbstractSocket::SocketError error) +{ + if (m_error == error) + return; + + m_error = error; + qCWarning(dcTunnelProxyRemoteConnection()) << "Error occured" << m_error; + emit errorOccured(m_error); +} + +void TunnelProxyRemoteConnection::cleanUp() +{ + if (m_jsonClient) { + m_jsonClient->deleteLater(); + m_jsonClient = nullptr; + } + + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + + m_remoteProxyServer.clear(); + m_remoteProxyServerName.clear(); + m_remoteProxyServerVersion.clear(); + m_remoteProxyApiVersion.clear(); + + setState(StateDisconnected); +} + +} diff --git a/libnymea-remoteproxyclient/tunnelproxy/tunnelproxyremoteconnection.h b/libnymea-remoteproxyclient/tunnelproxy/tunnelproxyremoteconnection.h index c649e74..b4cd1c4 100644 --- a/libnymea-remoteproxyclient/tunnelproxy/tunnelproxyremoteconnection.h +++ b/libnymea-remoteproxyclient/tunnelproxy/tunnelproxyremoteconnection.h @@ -1,16 +1,139 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, 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 Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser 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 TUNNELPROXYREMOTECONNECTION_H #define TUNNELPROXYREMOTECONNECTION_H +#include #include +#include + +#include "proxyconnection.h" + +Q_DECLARE_LOGGING_CATEGORY(dcTunnelProxyRemoteConnection) + +namespace remoteproxyclient { + +class JsonRpcClient; class TunnelProxyRemoteConnection : public QObject { Q_OBJECT public: - explicit TunnelProxyRemoteConnection(QObject *parent = nullptr); + + enum State { + StateConnecting, + StateHostLookup, + StateConnected, + StateInitializing, + StateRegister, + StateRemoteConnected, + StateDiconnecting, + StateDisconnected, + }; + Q_ENUM(State) + + enum ConnectionType { + ConnectionTypeWebSocket, + ConnectionTypeTcpSocket + }; + Q_ENUM(ConnectionType) + + explicit TunnelProxyRemoteConnection(const QUuid &clientUuid, const QString &clientName, QObject *parent = nullptr); + explicit TunnelProxyRemoteConnection(const QUuid &clientUuid, const QString &clientName, ConnectionType connectionType, QObject *parent = nullptr); + ~TunnelProxyRemoteConnection(); + + bool remoteConnected() const; + + QAbstractSocket::SocketError error() const; + + void ignoreSslErrors(); + void ignoreSslErrors(const QList &errors); + + QString remoteProxyServer() const; + QString remoteProxyServerName() const; + QString remoteProxyServerVersion() const; + QString remoteProxyApiVersion() const; + +public slots: + bool connectServer(const QUrl &url, const QUuid &serverUuid); + void disconnectServer(); signals: + void stateChanged(TunnelProxyRemoteConnection::State state); + void errorOccured(QAbstractSocket::SocketError error); + void sslErrors(const QList &errors); + + void remoteConnectedChanged(bool remoteConnected); + + void dataReady(const QByteArray &data); + +private slots: + void onConnectionChanged(bool connected); + void onConnectionDataAvailable(const QByteArray &data); + + void onConnectionSocketError(QAbstractSocket::SocketError error); + void onConnectionStateChanged(QAbstractSocket::SocketState state); + + // Initialization calls + void onHelloFinished(); + void onClientRegistrationFinished(); + +private: + // This server information + QUuid m_clientUuid; + QString m_clientName; + ConnectionType m_connectionType = ConnectionTypeTcpSocket; + + QUuid m_serverUuid; + + bool m_remoteConnected = false; + + // Remote proxy server information + QString m_remoteProxyServer; + QString m_remoteProxyServerName; + QString m_remoteProxyServerVersion; + QString m_remoteProxyApiVersion; + + QUrl m_serverUrl; + QAbstractSocket::SocketError m_error = QAbstractSocket::UnknownSocketError; + State m_state = StateDisconnected; + + ProxyConnection *m_connection = nullptr; + JsonRpcClient *m_jsonClient = nullptr; + + void setState(State state); + void setRemoteConnected(bool remoteConnected); + void setError(QAbstractSocket::SocketError error); + + void cleanUp(); }; +} + #endif // TUNNELPROXYREMOTECONNECTION_H diff --git a/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocket.cpp b/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocket.cpp index b033688..169d059 100644 --- a/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocket.cpp +++ b/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocket.cpp @@ -26,12 +26,63 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "tunnelproxysocket.h" +#include "proxyconnection.h" +#include "../common/slipdataprocessor.h" namespace remoteproxyclient { -TunnelProxySocket::TunnelProxySocket(QObject *parent) : QObject(parent) +TunnelProxySocket::TunnelProxySocket(ProxyConnection *connection, const QString &clientName, const QUuid &clientUuid, const QHostAddress &clientPeerAddress, quint16 socketAddress, QObject *parent) : + QObject(parent), + m_connection(connection), + m_clientName(clientName), + m_clientUuid(clientUuid), + m_clientPeerAddress(clientPeerAddress), + m_socketAddress(socketAddress) { } +QUuid TunnelProxySocket::clientUuid() const +{ + return m_clientUuid; +} + +QString TunnelProxySocket::clientName() const +{ + return m_clientName; +} + +QHostAddress TunnelProxySocket::clientPeerAddress() const +{ + return m_clientPeerAddress; +} + +quint16 TunnelProxySocket::socketAddress() const +{ + return m_socketAddress; +} + +void TunnelProxySocket::writeData(const QByteArray &data) +{ + SlipDataProcessor::Frame frame; + frame.socketAddress = m_socketAddress; + frame.data = data; + m_connection->sendData(SlipDataProcessor::serializeData(SlipDataProcessor::buildFrame(frame))); +} + +void TunnelProxySocket::disconnectSocket() +{ + +} + +QDebug operator<<(QDebug debug, TunnelProxySocket *tunnelProxySocket) +{ + debug.nospace() << "TunnelProxySocket("; + debug.nospace() << tunnelProxySocket->clientName() << ", "; + debug.nospace() << tunnelProxySocket->clientUuid().toString() << ", "; + debug.nospace() << tunnelProxySocket->clientPeerAddress().toString() << ", "; + debug.nospace() << tunnelProxySocket->socketAddress() << ")"; + return debug.space(); +} + } diff --git a/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocket.h b/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocket.h index bd259f9..bb3b6a7 100644 --- a/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocket.h +++ b/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocket.h @@ -28,20 +28,48 @@ #ifndef TUNNELPROXYSOCKET_H #define TUNNELPROXYSOCKET_H +#include #include +#include namespace remoteproxyclient { +class ProxyConnection; + class TunnelProxySocket : public QObject { Q_OBJECT + friend class TunnelProxySocketServer; + public: - explicit TunnelProxySocket(QObject *parent = nullptr); + QUuid clientUuid() const; + QString clientName() const; + QHostAddress clientPeerAddress() const; + quint16 socketAddress() const; + + void writeData(const QByteArray &data); + + void disconnectSocket(); signals: + void dataReceived(const QByteArray &data); + void disconnected(); + +private: + explicit TunnelProxySocket(ProxyConnection *connection, const QString &clientName, const QUuid &clientUuid, const QHostAddress &clientPeerAddress, quint16 socketAddress, QObject *parent = nullptr); + ~TunnelProxySocket() = default; + + ProxyConnection *m_connection = nullptr; + + QString m_clientName; + QUuid m_clientUuid; + QHostAddress m_clientPeerAddress; + quint16 m_socketAddress = 0xFFFF; }; +QDebug operator<<(QDebug debug, TunnelProxySocket *tunnelProxySocket); + } #endif // TUNNELPROXYSOCKET_H diff --git a/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocketserver.cpp b/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocketserver.cpp index 67576c2..616eea6 100644 --- a/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocketserver.cpp +++ b/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocketserver.cpp @@ -30,8 +30,10 @@ #include "tcpsocketconnection.h" #include "websocketconnection.h" #include "proxyjsonrpcclient.h" +#include "../common/slipdataprocessor.h" Q_LOGGING_CATEGORY(dcTunnelProxySocketServer, "dcTunnelProxySocketServer") +Q_LOGGING_CATEGORY(dcTunnelProxySocketServerTraffic, "dcTunnelProxySocketServerTraffic") namespace remoteproxyclient { @@ -67,10 +69,14 @@ QAbstractSocket::SocketError TunnelProxySocketServer::error() const return m_error; } +TunnelProxySocketServer::Error TunnelProxySocketServer::serverError() const +{ + return m_serverError; +} + void TunnelProxySocketServer::ignoreSslErrors() { m_connection->ignoreSslErrors(); - } void TunnelProxySocketServer::ignoreSslErrors(const QList &errors) @@ -78,6 +84,32 @@ void TunnelProxySocketServer::ignoreSslErrors(const QList &errors) m_connection->ignoreSslErrors(errors); } +QUrl TunnelProxySocketServer::serverUrl() const +{ + return m_serverUrl; +} + +QString TunnelProxySocketServer::remoteProxyServer() const +{ + return m_remoteProxyServer; +} + +QString TunnelProxySocketServer::remoteProxyServerName() const +{ + return m_remoteProxyServerName; +} + +QString TunnelProxySocketServer::remoteProxyServerVersion() const +{ + return m_remoteProxyServerVersion; +} + +QString TunnelProxySocketServer::remoteProxyApiVersion() const +{ + return m_remoteProxyApiVersion; +} + + void TunnelProxySocketServer::startServer(const QUrl &serverUrl) { m_serverUrl = serverUrl; @@ -103,7 +135,8 @@ void TunnelProxySocketServer::startServer(const QUrl &serverUrl) connect(m_connection, &ProxyConnection::sslErrors, this, &TunnelProxySocketServer::sslErrors); m_jsonClient = new JsonRpcClient(m_connection, this); -// connect(m_jsonClient, &JsonRpcClient::tunnelEstablished, this, &RemoteProxyConnection::onTunnelEstablished); + connect(m_jsonClient, &JsonRpcClient::tunnelProxyClientConnected, this, &TunnelProxySocketServer::onTunnelProxyClientConnected); + connect(m_jsonClient, &JsonRpcClient::tunnelProxyClientDisonnected, this, &TunnelProxySocketServer::onTunnelProxyClientDisconnected); qCDebug(dcTunnelProxySocketServer()) << "Connecting to" << m_serverUrl.toString(); m_connection->connectServer(m_serverUrl); @@ -111,7 +144,10 @@ void TunnelProxySocketServer::startServer(const QUrl &serverUrl) void TunnelProxySocketServer::stopServer() { - // Close the server connection and all related sockets + if (m_connection) { + qCDebug(dcTunnelProxySocketServer()) << "Disconnecting from" << m_connection->serverUrl().toString(); + m_connection->disconnectServer(); + } } void TunnelProxySocketServer::onConnectionChanged(bool connected) @@ -119,6 +155,7 @@ void TunnelProxySocketServer::onConnectionChanged(bool connected) if (connected) { qCDebug(dcTunnelProxySocketServer()) << "Connected to remote proxy server."; setState(StateConnected); + m_serverError = TunnelProxySocketServer::ErrorNoError; setState(StateInitializing); JsonReply *reply = m_jsonClient->callHello(); @@ -126,18 +163,53 @@ void TunnelProxySocketServer::onConnectionChanged(bool connected) } else { qCDebug(dcTunnelProxySocketServer()) << "Disconnected from remote proxy server."; setState(StateDisconnected); + setServerError(ErrorNotConnected); cleanUp(); } } void TunnelProxySocketServer::onConnectionDataAvailable(const QByteArray &data) { + qCDebug(dcTunnelProxySocketServerTraffic()) << "Data received" << qUtf8Printable(data); + if (m_state != StateRunning) { m_jsonClient->processData(data); + m_dataBuffer.clear(); return; } - // Parse SLIP + // Parse SLIP frame + for (int i = 0; i < data.length(); i++) { + quint8 byte = static_cast(data.at(i)); + if (byte == SlipDataProcessor::ProtocolByteEnd) { + // If there is no data...continue since it might be a starting END byte + if (m_dataBuffer.isEmpty()) + continue; + + QByteArray frameData = SlipDataProcessor::deserializeData(m_dataBuffer); + if (frameData.isNull()) { + qCWarning(dcTunnelProxySocketServerTraffic()) << "Received inconsistant SLIP encoded message. Ignoring data..."; + } else { + SlipDataProcessor::Frame frame = SlipDataProcessor::parseFrame(frameData); + qCDebug(dcTunnelProxySocketServerTraffic()) << "Frame received" << frame.socketAddress << qUtf8Printable(frame.data); + if (frame.socketAddress == 0x0000) { + m_jsonClient->processData(frame.data); + } else { + // Find the socket and emit the data received signal + TunnelProxySocket *tunnlProxySocket = m_tunnelProxySockets.value(frame.socketAddress); + if (!tunnlProxySocket) { + qCWarning(dcTunnelProxySocketServer()) << "Received data from unknown tunnel proxy client with address" << frame.socketAddress << "...ignoring the data"; + } else { + emit tunnlProxySocket->dataReceived(frame.data); + } + } + } + + m_dataBuffer.clear(); + } else { + m_dataBuffer.append(data.at(i)); + } + } } void TunnelProxySocketServer::onConnectionSocketError(QAbstractSocket::SocketError error) @@ -181,6 +253,7 @@ void TunnelProxySocketServer::onHelloFinished() if (response.value("status").toString() != "success") { qCWarning(dcTunnelProxySocketServer()) << "Could not get initial information from proxy server."; m_connection->disconnectServer(); + setServerError(ErrorConnectionError); return; } @@ -207,6 +280,7 @@ void TunnelProxySocketServer::onServerRegistrationFinished() if (response.value("status").toString() != "success") { qCWarning(dcTunnelProxySocketServer()) << "JSON RPC error. Failed to register as tunnel server on the remote proxy server."; m_connection->disconnectServer(); + setServerError(ErrorConnectionError); return; } @@ -214,11 +288,34 @@ void TunnelProxySocketServer::onServerRegistrationFinished() if (responseParams.value("tunnelProxyError").toString() != "TunnelProxyErrorNoError") { qCWarning(dcTunnelProxySocketServer()) << "Failed to register as tunnel server on the remote proxy server:" << responseParams.value("tunnelProxyError").toString(); m_connection->disconnectServer(); + setServerError(ErrorConnectionError); return; } qCDebug(dcTunnelProxySocketServer()) << "Registered successfully as tunnel server on the remote proxy server."; setState(StateRunning); + m_serverError = ErrorNoError; +} + +void TunnelProxySocketServer::onTunnelProxyClientConnected(const QString &clientName, const QUuid &clientUuid, const QString &clientPeerAddress, quint16 socketAddress) +{ + TunnelProxySocket *tunnelProxySocket = new TunnelProxySocket(m_connection, clientName, clientUuid, QHostAddress(clientPeerAddress), socketAddress, this); + qCDebug(dcTunnelProxySocketServer()) << "--> New client connected" << tunnelProxySocket; + m_tunnelProxySockets.insert(socketAddress, tunnelProxySocket); + emit clientConnected(tunnelProxySocket); +} + +void TunnelProxySocketServer::onTunnelProxyClientDisconnected(quint16 socketAddress) +{ + TunnelProxySocket *tunnelProxySocket = m_tunnelProxySockets.take(socketAddress); + if (!tunnelProxySocket) { + qCWarning(dcTunnelProxySocketServer()) << "Unknown client disconnected" << socketAddress << ". Ignoring the event."; + return; + } + + qCDebug(dcTunnelProxySocketServer()) << "--> Client disconnected" << tunnelProxySocket; + emit tunnelProxySocket->disconnected(); + tunnelProxySocket->deleteLater(); } void TunnelProxySocketServer::setState(State state) @@ -253,10 +350,21 @@ void TunnelProxySocketServer::setError(QAbstractSocket::SocketError error) emit errorOccured(m_error); } +void TunnelProxySocketServer::setServerError(Error error) +{ + if (m_serverError == error) + return; + + qCDebug(dcTunnelProxySocketServer()) << "Server error occured" << error; + m_serverError = error; + emit serverErrorOccured(m_serverError); +} + void TunnelProxySocketServer::cleanUp() { - // TODO: cleanup client connections - + foreach (quint16 socketAddress, m_tunnelProxySockets.keys()) { + onTunnelProxyClientDisconnected(socketAddress); + } if (m_jsonClient) { m_jsonClient->deleteLater(); @@ -268,8 +376,10 @@ void TunnelProxySocketServer::cleanUp() m_connection = nullptr; } -// m_serverName = QString(); -// m_serverUuid = QUuid(); + m_remoteProxyServer.clear(); + m_remoteProxyServerName.clear(); + m_remoteProxyServerVersion.clear(); + m_remoteProxyApiVersion.clear(); setState(StateDisconnected); } diff --git a/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocketserver.h b/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocketserver.h index 15b4926..7a20bf2 100644 --- a/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocketserver.h +++ b/libnymea-remoteproxyclient/tunnelproxy/tunnelproxysocketserver.h @@ -32,14 +32,14 @@ #include #include -Q_DECLARE_LOGGING_CATEGORY(dcTunnelProxySocketServer) - #include "proxyconnection.h" #include "tunnelproxysocket.h" +Q_DECLARE_LOGGING_CATEGORY(dcTunnelProxySocketServer) +Q_DECLARE_LOGGING_CATEGORY(dcTunnelProxySocketServerTraffic) + namespace remoteproxyclient { -class ProxyConnection; class JsonRpcClient; class TunnelProxySocketServer : public QObject @@ -64,6 +64,15 @@ public: }; Q_ENUM(ConnectionType) + enum Error { + ErrorNoError, + ErrorConnectionError, + ErrorInvalidAddress, + ErrorNotConnected, + ErrorNotRegistered + }; + Q_ENUM(Error) + explicit TunnelProxySocketServer(const QUuid &serverUuid, const QString &serverName, QObject *parent = nullptr); explicit TunnelProxySocketServer(const QUuid &serverUuid, const QString &serverName, ConnectionType connectionType, QObject *parent = nullptr); ~TunnelProxySocketServer(); @@ -71,21 +80,32 @@ public: bool running() const; QAbstractSocket::SocketError error() const; + Error serverError() const; void ignoreSslErrors(); void ignoreSslErrors(const QList &errors); + QUrl serverUrl() const; + + QString remoteProxyServer() const; + QString remoteProxyServerName() const; + QString remoteProxyServerVersion() const; + QString remoteProxyApiVersion() const; + public slots: void startServer(const QUrl &serverUrl); void stopServer(); signals: - void runningChanged(bool running); - void stateChanged(TunnelProxySocketServer::State state); void errorOccured(QAbstractSocket::SocketError error); + void serverErrorOccured(Error error); void sslErrors(const QList &errors); + void runningChanged(bool running); + + void clientConnected(TunnelProxySocket *tunnelProxySocket); + void clientDisconnected(TunnelProxySocket *tunnelProxySocket); private slots: void onConnectionChanged(bool connected); @@ -98,6 +118,10 @@ private slots: void onHelloFinished(); void onServerRegistrationFinished(); + // Client notifications + void onTunnelProxyClientConnected(const QString &clientName, const QUuid &clientUuid, const QString &clientPeerAddress, quint16 socketAddress); + void onTunnelProxyClientDisconnected(quint16 socketAddress); + private: // This server information QUuid m_serverUuid; @@ -113,14 +137,20 @@ private: bool m_running = false; QUrl m_serverUrl; QAbstractSocket::SocketError m_error = QAbstractSocket::UnknownSocketError; + Error m_serverError = ErrorNoError; State m_state = StateDisconnected; ProxyConnection *m_connection = nullptr; JsonRpcClient *m_jsonClient = nullptr; + QHash m_tunnelProxySockets; + + QByteArray m_dataBuffer; + void setState(State state); void setRunning(bool running); void setError(QAbstractSocket::SocketError error); + void setServerError(Error error); void cleanUp(); }; diff --git a/nymea-remoteproxy.pro b/nymea-remoteproxy.pro index 2e38692..bdd60cf 100644 --- a/nymea-remoteproxy.pro +++ b/nymea-remoteproxy.pro @@ -4,11 +4,11 @@ TEMPLATE=subdirs SUBDIRS += server client libnymea-remoteproxy libnymea-remoteproxyclient !disabletests { - SUBDIRS+=tests + SUBDIRS += tests } !disablemonitor { - SUBDIRS+=monitor + SUBDIRS += monitor } server.depends = libnymea-remoteproxy @@ -26,8 +26,14 @@ message("----------------------------------------------------------") message("JSON-RPC API version $${API_VERSION_MAJOR}.$${API_VERSION_MINOR}") message("Qt version:" $$[QT_VERSION]) -coverage { message("Building with coverage report") } -ccache { message("Building with ccache support") } +coverage { + message("Building with coverage report") +} + +ccache { + message("Building with ccache support") +} + disablemonitor { message("Building without the monitor") } diff --git a/tests/test-tunnelproxy/remoteproxyteststunnelproxy.cpp b/tests/test-tunnelproxy/remoteproxyteststunnelproxy.cpp index f8217bf..0ff10c6 100644 --- a/tests/test-tunnelproxy/remoteproxyteststunnelproxy.cpp +++ b/tests/test-tunnelproxy/remoteproxyteststunnelproxy.cpp @@ -33,6 +33,7 @@ // Client #include "tunnelproxy/tunnelproxysocketserver.h" +#include "tunnelproxy/tunnelproxyremoteconnection.h" #include #include @@ -296,6 +297,8 @@ void RemoteProxyTestsTunnelProxy::registerClient() QVERIFY(!response.isEmpty()); QVERIFY(response.value("status").toString() == "success"); QVERIFY(response.value("params").toMap().contains("tunnelProxyError")); + QVERIFY(response.value("params").toMap().contains("slipEnabled")); + //QVERIFY(response.value("params").toMap().value("slipEnabled").toBool())); verifyTunnelProxyError(response, expectedServerError); } @@ -358,27 +361,24 @@ void RemoteProxyTestsTunnelProxy::registerServerDuplicated() QVERIFY(!response.isEmpty()); QVERIFY(response.value("status").toString() == "success"); QVERIFY(response.value("params").toMap().contains("tunnelProxyError")); + QVERIFY(response.value("params").toMap().contains("slipEnabled")); + QVERIFY(response.value("params").toMap().value("slipEnabled").toBool()); verifyTunnelProxyError(response); + // SLIP Should be enabled now + // Try to register again with the same uuid on the same socket - result = invokeTcpSocketTunnelProxyApiCallPersistant("TunnelProxy.RegisterServer", params, socket); + result = invokeTcpSocketTunnelProxyApiCallPersistant("TunnelProxy.RegisterServer", params, true, socket); response = result.first.toMap(); QVERIFY(response.value("status").toString() == "success"); QVERIFY(response.value("params").toMap().contains("tunnelProxyError")); verifyTunnelProxyError(response, TunnelProxyServer::TunnelProxyErrorAlreadyRegistered); - // Try to register an invalid server uuid - QVariantMap paramsInvalidUuid; - paramsInvalidUuid.insert("serverName", serverName); - paramsInvalidUuid.insert("serverUuid", QUuid().toString()); - result = invokeTcpSocketTunnelProxyApiCallPersistant("TunnelProxy.RegisterServer", paramsInvalidUuid, socket); - response = result.first.toMap(); - QVERIFY(response.value("status").toString() == "success"); - QVERIFY(response.value("params").toMap().contains("tunnelProxyError")); - verifyTunnelProxyError(response, TunnelProxyServer::TunnelProxyErrorInvalidUuid); + QSignalSpy disconnectedTcpSpy(socket, &QSslSocket::disconnected); + QVERIFY(disconnectedTcpSpy.wait()); + QVERIFY(disconnectedTcpSpy.count() == 1); // Close the tcp socket - socket->close(); delete socket; QTest::qWait(100); @@ -394,18 +394,16 @@ void RemoteProxyTestsTunnelProxy::registerServerDuplicated() verifyTunnelProxyError(response); // Try to register again with the same uuid on the same socket - resultWebSocket = invokeWebSocketTunnelProxyApiCallPersistant("TunnelProxy.RegisterServer", params, webSocket); + resultWebSocket = invokeWebSocketTunnelProxyApiCallPersistant("TunnelProxy.RegisterServer", params, true, webSocket); response = resultWebSocket.first.toMap(); QVERIFY(response.value("status").toString() == "success"); QVERIFY(response.value("params").toMap().contains("tunnelProxyError")); verifyTunnelProxyError(response, TunnelProxyServer::TunnelProxyErrorAlreadyRegistered); - // Try to register an invalid server uuid - resultWebSocket = invokeWebSocketTunnelProxyApiCallPersistant("TunnelProxy.RegisterServer", paramsInvalidUuid, webSocket); - response = resultWebSocket.first.toMap(); - QVERIFY(response.value("status").toString() == "success"); - QVERIFY(response.value("params").toMap().contains("tunnelProxyError")); - verifyTunnelProxyError(response, TunnelProxyServer::TunnelProxyErrorInvalidUuid); + + QSignalSpy disconnectedWebSocketSpy(webSocket, &QWebSocket::disconnected); + QVERIFY(disconnectedWebSocketSpy.wait()); + QVERIFY(disconnectedWebSocketSpy.count() == 1); webSocket->close(); delete webSocket; @@ -470,7 +468,6 @@ void RemoteProxyTestsTunnelProxy::registerClientDuplicated() QVERIFY(response.value("params").toMap().contains("tunnelProxyError")); verifyTunnelProxyError(response, TunnelProxyServer::TunnelProxyErrorAlreadyRegistered); - // CleanUp if (clientSocketTcp) { // Close the tcp socket @@ -501,8 +498,10 @@ void RemoteProxyTestsTunnelProxy::crossRegisterServerClient() // Start the server startServer(); + resetDebugCategories(); resetDebugCategories(); addDebugCategory("TunnelProxyServer.debug=true"); + addDebugCategory("TunnelProxyServerTraffic.debug=true"); addDebugCategory("JsonRpcTraffic.debug=true"); // Create the server and keep it up @@ -512,7 +511,7 @@ void RemoteProxyTestsTunnelProxy::crossRegisterServerClient() serverParams.insert("serverName", serverName); serverParams.insert("serverUuid", serverUuid.toString()); - // TCP socket + // TCP server QPair serverResult = invokeTcpSocketTunnelProxyApiCallPersistant("TunnelProxy.RegisterServer", serverParams); QVariantMap response = serverResult.first.toMap(); QSslSocket *serverSocket = serverResult.second; @@ -521,6 +520,7 @@ void RemoteProxyTestsTunnelProxy::crossRegisterServerClient() QVERIFY(response.value("params").toMap().contains("tunnelProxyError")); verifyTunnelProxyError(response); + // SLIP is enabled now // Now try to register as client QVariantMap clientParams; @@ -528,13 +528,17 @@ void RemoteProxyTestsTunnelProxy::crossRegisterServerClient() clientParams.insert("clientUuid", QUuid::createUuid().toString()); clientParams.insert("serverUuid", serverUuid.toString()); - QPair result = invokeTcpSocketTunnelProxyApiCallPersistant("TunnelProxy.RegisterClient", clientParams, serverSocket); + QPair result = invokeTcpSocketTunnelProxyApiCallPersistant("TunnelProxy.RegisterClient", clientParams, true, serverSocket); response = result.first.toMap(); QVERIFY(!response.isEmpty()); QVERIFY(response.value("status").toString() == "success"); QVERIFY(response.value("params").toMap().contains("tunnelProxyError")); verifyTunnelProxyError(response, TunnelProxyServer::TunnelProxyErrorAlreadyRegistered); + QSignalSpy disconnectedSpy(serverSocket, &QSslSocket::disconnected); + QVERIFY(disconnectedSpy.wait()); + QVERIFY(disconnectedSpy.count() == 1); + serverSocket->close(); delete serverSocket; @@ -551,8 +555,79 @@ void RemoteProxyTestsTunnelProxy::testTunnelProxyServer() resetDebugCategories(); addDebugCategory("TunnelProxyServer.debug=true"); + addDebugCategory("TunnelProxyServerTraffic.debug=true"); addDebugCategory("JsonRpcTraffic.debug=true"); addDebugCategory("TunnelProxySocketServer.debug=true"); + addDebugCategory("TunnelProxyRemoteConnection.debug=true"); + addDebugCategory("RemoteProxyClientJsonRpcTraffic.debug=true"); + + // Tunnel proxy socket server + QString serverName = "SuperDuper server name"; + QUuid serverUuid = QUuid::createUuid(); + + TunnelProxySocketServer *tunnelProxyServer = new TunnelProxySocketServer(serverUuid, serverName, this); + connect(tunnelProxyServer, &TunnelProxySocketServer::sslErrors, this, [=](const QList &errors){ + tunnelProxyServer->ignoreSslErrors(errors); + }); + + tunnelProxyServer->startServer(m_serverUrlTunnelProxyTcp); + + QSignalSpy serverRunningSpy(tunnelProxyServer, &TunnelProxySocketServer::runningChanged); + serverRunningSpy.wait(); + QVERIFY(serverRunningSpy.count() == 1); + QList arguments = serverRunningSpy.takeFirst(); + QVERIFY(arguments.at(0).toBool() == true); + QVERIFY(tunnelProxyServer->running()); + + // Tunnel proxy client connection + QString clientName = "Awesome client name"; + QUuid clientUuid = QUuid::createUuid(); + + TunnelProxyRemoteConnection *clientConnection = new TunnelProxyRemoteConnection(clientUuid, clientName, this); + connect(clientConnection, &TunnelProxyRemoteConnection::sslErrors, this, [=](const QList &errors){ + clientConnection->ignoreSslErrors(errors); + }); + + clientConnection->connectServer(m_serverUrlTunnelProxyTcp, serverUuid); + + QSignalSpy clientConnectedSpy(tunnelProxyServer, &TunnelProxySocketServer::clientConnected); + QVERIFY(clientConnectedSpy.wait()); + QVERIFY(clientConnectedSpy.count() == 1); + arguments = clientConnectedSpy.takeFirst(); + TunnelProxySocket *tunnelProxySocket = arguments.at(0).value(); + QVERIFY(tunnelProxySocket->clientName() == clientName); + QVERIFY(tunnelProxySocket->clientUuid() == clientUuid); + + // Stop the server connection and verify the client gets disconnected + + tunnelProxyServer->stopServer(); + + QSignalSpy disconnectedSpy(clientConnection, &TunnelProxyRemoteConnection::remoteConnectedChanged); + QVERIFY(disconnectedSpy.wait()); + QVERIFY(disconnectedSpy.count() == 1); + arguments = disconnectedSpy.takeFirst(); + QVERIFY(arguments.at(0).toBool() == false); + QVERIFY(!clientConnection->remoteConnected()); + + + resetDebugCategories(); + + // Clean up + stopServer(); +} + +void RemoteProxyTestsTunnelProxy::testTunnelProxyClient() +{ + // Start the server + startServer(); + + resetDebugCategories(); + addDebugCategory("TunnelProxyServer.debug=true"); + addDebugCategory("TunnelProxyServerTraffic.debug=true"); + addDebugCategory("JsonRpcTraffic.debug=true"); + addDebugCategory("TunnelProxySocketServer.debug=true"); + addDebugCategory("TunnelProxyRemoteConnection.debug=true"); + addDebugCategory("RemoteProxyClientJsonRpcTraffic.debug=true"); // Tunnel proxy socket server QString serverName = "SuperDuper server name"; @@ -574,6 +649,24 @@ void RemoteProxyTestsTunnelProxy::testTunnelProxyServer() // Tunnel proxy client connection + QString clientName = "Awesome client name"; + QUuid clientUuid = QUuid::createUuid(); + + TunnelProxyRemoteConnection *clientConnection = new TunnelProxyRemoteConnection(clientUuid, clientName, this); + connect(clientConnection, &TunnelProxyRemoteConnection::sslErrors, this, [=](const QList &errors){ + clientConnection->ignoreSslErrors(errors); + }); + + clientConnection->connectServer(m_serverUrlTunnelProxyTcp, serverUuid); + + QSignalSpy clientRemoteConnectedSpy(clientConnection, &TunnelProxyRemoteConnection::remoteConnectedChanged); + QVERIFY(clientRemoteConnectedSpy.wait()); + QVERIFY(clientRemoteConnectedSpy.count() == 1); + arguments = clientRemoteConnectedSpy.takeFirst(); + QVERIFY(arguments.at(0).toBool() == true); + QVERIFY(clientConnection->remoteConnected()); + + // Verify server socket connected diff --git a/tests/test-tunnelproxy/remoteproxyteststunnelproxy.h b/tests/test-tunnelproxy/remoteproxyteststunnelproxy.h index c4d40cb..8c0ca6b 100644 --- a/tests/test-tunnelproxy/remoteproxyteststunnelproxy.h +++ b/tests/test-tunnelproxy/remoteproxyteststunnelproxy.h @@ -66,6 +66,7 @@ private slots: // Client classes void testTunnelProxyServer(); + void testTunnelProxyClient(); }; diff --git a/tests/testbase/basetest.cpp b/tests/testbase/basetest.cpp index cd55fef..e625ef7 100644 --- a/tests/testbase/basetest.cpp +++ b/tests/testbase/basetest.cpp @@ -31,6 +31,8 @@ #include "loggingcategories.h" #include "remoteproxyconnection.h" +#include "../common/slipdataprocessor.h" + #include #include #include @@ -656,7 +658,7 @@ QVariant BaseTest::injectTcpSocketTunnelProxyData(const QByteArray &data) return jsonDoc.toVariant(); } -QPair BaseTest::invokeTcpSocketTunnelProxyApiCallPersistant(const QString &method, const QVariantMap params, QSslSocket *existingSocket) +QPair BaseTest::invokeTcpSocketTunnelProxyApiCallPersistant(const QString &method, const QVariantMap params, bool slipEnabled, QSslSocket *existingSocket) { QSslSocket *socket = nullptr; if (!existingSocket) { @@ -680,30 +682,66 @@ QPair BaseTest::invokeTcpSocketTunnelProxyApiCallPersist request.insert("method", method); request.insert("params", params); QJsonDocument jsonDoc = QJsonDocument::fromVariant(request); - + QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact) + '\n'; QSignalSpy dataSpy(socket, &QSslSocket::readyRead); - socket->write(jsonDoc.toJson(QJsonDocument::Compact) + '\n'); + + if (slipEnabled) { + SlipDataProcessor::Frame frame; + frame.socketAddress = 0x0000; + frame.data = payload; + socket->write(SlipDataProcessor::serializeData(SlipDataProcessor::buildFrame(frame))); + } else { + socket->write(payload); + } + // FIXME: check why it waits the full time here dataSpy.wait(500); - if (dataSpy.count() != 1) { + if (dataSpy.count() < 1) { qWarning() << "No data received"; return QPair(QVariant(), socket); } - QByteArray data = socket->readAll(); + QByteArray data; + if (slipEnabled) { + QByteArray rawData = socket->readAll(); + QByteArray messageData; + // Parse SLIP frame + for (int i = 0; i < rawData.length(); i++) { + quint8 byte = static_cast(rawData.at(i)); + if (byte == SlipDataProcessor::ProtocolByteEnd) { + // If there is no data...continue since it might be a starting END byte + if (messageData.isEmpty()) + continue; - // Make sure the response ends with '}\n' - if (!data.endsWith("}\n")) { - qWarning() << "JSON data does not end with \"}\n\""; - return QPair(QVariant(), socket); + break; + messageData.clear(); + } else { + messageData.append(rawData.at(i)); + } + } + + QByteArray deserializedData = SlipDataProcessor::deserializeData(messageData); + SlipDataProcessor::Frame frame = SlipDataProcessor::parseFrame(deserializedData); + if (frame.data.isEmpty()) { + qWarning() << "Could not deserialize SLIP data"; + return QPair(QVariant(), socket); + } + data = frame.data; + } else { + data = socket->readAll(); + // Make sure the response ends with '}\n' + if (!data.endsWith("}\n")) { + qWarning() << "JSON data does not end with \"}\n\""; + return QPair(QVariant(), socket); + } } // 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(); + qWarning() << "JSON parser error" << error.errorString() << qUtf8Printable(data); return QPair(QVariant(), socket); } @@ -718,7 +756,7 @@ QPair BaseTest::invokeTcpSocketTunnelProxyApiCallPersist return QPair(QVariant(), socket); } -QPair BaseTest::invokeWebSocketTunnelProxyApiCallPersistant(const QString &method, const QVariantMap params, QWebSocket *existingSocket) +QPair BaseTest::invokeWebSocketTunnelProxyApiCallPersistant(const QString &method, const QVariantMap params, bool slipEnabled, QWebSocket *existingSocket) { QWebSocket *socket = nullptr; if (!existingSocket) { @@ -740,16 +778,55 @@ QPair BaseTest::invokeWebSocketTunnelProxyApiCallPersist request.insert("method", method); request.insert("params", params); QJsonDocument jsonDoc = QJsonDocument::fromVariant(request); + QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact) + '\n'; QSignalSpy dataSpy(socket, SIGNAL(textMessageReceived(QString))); - socket->sendTextMessage(QString(jsonDoc.toJson(QJsonDocument::Compact))); + + if (slipEnabled) { + SlipDataProcessor::Frame frame; + frame.socketAddress = 0x0000; + frame.data = payload; + socket->sendTextMessage(QString(SlipDataProcessor::serializeData(SlipDataProcessor::buildFrame(frame)))); + } else { + socket->sendTextMessage(QString(payload)); + } + dataSpy.wait(); for (int i = 0; i < dataSpy.count(); i++) { - // Make sure the response ends with '}\n' - if (!dataSpy.at(i).last().toByteArray().endsWith("}\n")) { - qWarning() << "JSON data does not end with \"}\n\""; - return QPair(QVariant(), socket); + QByteArray data; + if (slipEnabled) { + QByteArray rawData = dataSpy.at(i).last().toByteArray(); + QByteArray messageData; + // Parse SLIP frame + for (int i = 0; i < rawData.length(); i++) { + quint8 byte = static_cast(rawData.at(i)); + if (byte == SlipDataProcessor::ProtocolByteEnd) { + // If there is no data...continue since it might be a starting END byte + if (messageData.isEmpty()) + continue; + + break; + messageData.clear(); + } else { + messageData.append(rawData.at(i)); + } + } + + QByteArray deserializedData = SlipDataProcessor::deserializeData(messageData); + SlipDataProcessor::Frame frame = SlipDataProcessor::parseFrame(deserializedData); + if (frame.data.isEmpty()) { + qWarning() << "Could not deserialize SLIP data"; + return QPair(QVariant(), socket); + } + data = frame.data; + } else { + data = dataSpy.at(i).last().toByteArray(); + // Make sure the response ends with '}\n' + if (!data.endsWith("}\n")) { + qWarning() << "JSON data does not end with \"}\n\""; + return QPair(QVariant(), socket); + } } // Make sure the response it a valid JSON string @@ -771,6 +848,7 @@ QPair BaseTest::invokeWebSocketTunnelProxyApiCallPersist } } + qWarning() << "No websocket data received..."; m_commandCounter++; return QPair(QVariant(), socket); } diff --git a/tests/testbase/basetest.h b/tests/testbase/basetest.h index 5d321e2..57cebbd 100644 --- a/tests/testbase/basetest.h +++ b/tests/testbase/basetest.h @@ -97,8 +97,8 @@ protected: QVariant invokeTcpSocketTunnelProxyApiCall(const QString &method, const QVariantMap params = QVariantMap()); QVariant injectTcpSocketTunnelProxyData(const QByteArray &data); - QPair invokeTcpSocketTunnelProxyApiCallPersistant(const QString &method, const QVariantMap params = QVariantMap(), QSslSocket *existingSocket = nullptr); - QPair invokeWebSocketTunnelProxyApiCallPersistant(const QString &method, const QVariantMap params = QVariantMap(), QWebSocket *existingSocket = nullptr); + QPair invokeTcpSocketTunnelProxyApiCallPersistant(const QString &method, const QVariantMap params = QVariantMap(), bool slipEnabled = false, QSslSocket *existingSocket = nullptr); + QPair invokeWebSocketTunnelProxyApiCallPersistant(const QString &method, const QVariantMap params = QVariantMap(), bool slipEnabled = false, QWebSocket *existingSocket = nullptr); bool createRemoteConnection(const QString &token, const QString &nonce, QObject *parent); diff --git a/tests/testbase/testbase.pri b/tests/testbase/testbase.pri index 723b56d..f5e30df 100644 --- a/tests/testbase/testbase.pri +++ b/tests/testbase/testbase.pri @@ -1,5 +1,7 @@ RESOURCES += ../resources/resources.qrc +include(../common/common.pri) + CONFIG += testcase QT += testlib