From acd48bf7bf20b909a458d046c4b94c7ebf9c945a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 28 Jul 2021 14:55:18 +0200 Subject: [PATCH] Restructure the project and introduce the new tunnel proxy --- README.md | 141 ++++++----- debian/control | 25 +- debian/copyright | 23 +- debian/nymea-remoteproxy-tests.install | 3 +- docs/remote-connection-basic-flow.png | Bin 0 -> 92607 bytes docs/remote-connection-basic-flow.svg | 1 + docs/remote-connection-basic-flow.txt | 33 +++ libnymea-remoteproxy/engine.cpp | 92 +++++-- libnymea-remoteproxy/engine.h | 33 ++- .../jsonrpc/authenticationhandler.cpp | 2 +- libnymea-remoteproxy/jsonrpc/jsontypes.cpp | 11 +- libnymea-remoteproxy/jsonrpc/jsontypes.h | 2 + .../jsonrpc/tunnelproxyhandler.cpp | 72 ++++++ .../jsonrpc/tunnelproxyhandler.h | 55 +++++ libnymea-remoteproxy/libnymea-remoteproxy.pro | 36 +-- libnymea-remoteproxy/logengine.cpp | 10 +- libnymea-remoteproxy/logengine.h | 2 +- libnymea-remoteproxy/loggingcategories.cpp | 5 +- libnymea-remoteproxy/loggingcategories.h | 5 +- .../{ => proxy}/proxyserver.cpp | 11 +- .../{ => proxy}/proxyserver.h | 10 +- .../{ => proxy}/tunnelconnection.cpp | 2 +- .../{ => proxy}/tunnelconnection.h | 0 libnymea-remoteproxy/proxyclient.cpp | 2 +- libnymea-remoteproxy/proxyclient.h | 2 +- libnymea-remoteproxy/proxyconfiguration.cpp | 66 ++++- libnymea-remoteproxy/proxyconfiguration.h | 36 ++- .../{ => server}/jsonrpcserver.cpp | 7 - .../{ => server}/jsonrpcserver.h | 6 +- .../{ => server}/monitorserver.cpp | 0 .../{ => server}/monitorserver.h | 0 .../{ => server}/tcpsocketserver.cpp | 50 ++-- .../{ => server}/tcpsocketserver.h | 0 .../{ => server}/transportinterface.cpp | 0 .../{ => server}/transportinterface.h | 0 .../{ => server}/websocketserver.cpp | 0 .../{ => server}/websocketserver.h | 0 .../tunnelproxy/tunnelproxymanager.cpp | 141 +++++++++++ .../tunnelproxy/tunnelproxymanager.h | 93 +++++++ .../tunnelproxy/tunnelproxyserver.cpp | 56 +++++ .../tunnelproxy/tunnelproxyserver.h | 59 +++++ monitor/main.cpp | 2 +- monitor/terminalwindow.cpp | 2 +- server/main.cpp | 4 +- .../remoteproxytestsproxy.cpp} | 164 ++++++------- .../remoteproxytestsproxy.h} | 15 +- .../test-proxy.pro} | 6 +- .../remoteproxyteststunnelproxy.cpp | 202 +++++++++++++++ .../remoteproxyteststunnelproxy.h | 59 +++++ tests/test-tunnelproxy/test-tunnelproxy.pro | 14 ++ tests/testbase/basetest.cpp | 232 ++++++++++++++++-- tests/testbase/basetest.h | 18 +- tests/tests.pro | 10 +- 53 files changed, 1490 insertions(+), 330 deletions(-) create mode 100644 docs/remote-connection-basic-flow.png create mode 100644 docs/remote-connection-basic-flow.svg create mode 100644 docs/remote-connection-basic-flow.txt create mode 100644 libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.cpp create mode 100644 libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.h rename libnymea-remoteproxy/{ => proxy}/proxyserver.cpp (97%) rename libnymea-remoteproxy/{ => proxy}/proxyserver.h (97%) rename libnymea-remoteproxy/{ => proxy}/tunnelconnection.cpp (97%) rename libnymea-remoteproxy/{ => proxy}/tunnelconnection.h (100%) rename libnymea-remoteproxy/{ => server}/jsonrpcserver.cpp (98%) rename libnymea-remoteproxy/{ => server}/jsonrpcserver.h (99%) rename libnymea-remoteproxy/{ => server}/monitorserver.cpp (100%) rename libnymea-remoteproxy/{ => server}/monitorserver.h (100%) rename libnymea-remoteproxy/{ => server}/tcpsocketserver.cpp (75%) rename libnymea-remoteproxy/{ => server}/tcpsocketserver.h (100%) rename libnymea-remoteproxy/{ => server}/transportinterface.cpp (100%) rename libnymea-remoteproxy/{ => server}/transportinterface.h (100%) rename libnymea-remoteproxy/{ => server}/websocketserver.cpp (100%) rename libnymea-remoteproxy/{ => server}/websocketserver.h (100%) create mode 100644 libnymea-remoteproxy/tunnelproxy/tunnelproxymanager.cpp create mode 100644 libnymea-remoteproxy/tunnelproxy/tunnelproxymanager.h create mode 100644 libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.cpp create mode 100644 libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.h rename tests/{test-offline/nymea-remoteproxy-tests-offline.cpp => test-proxy/remoteproxytestsproxy.cpp} (91%) rename tests/{test-offline/nymea-remoteproxy-tests-offline.h => test-proxy/remoteproxytestsproxy.h} (89%) rename tests/{test-offline/test-offline.pro => test-proxy/test-proxy.pro} (55%) create mode 100644 tests/test-tunnelproxy/remoteproxyteststunnelproxy.cpp create mode 100644 tests/test-tunnelproxy/remoteproxyteststunnelproxy.h create mode 100644 tests/test-tunnelproxy/test-tunnelproxy.pro diff --git a/README.md b/README.md index a6806eb..12b207e 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ In order to get information about the server you can start the command with the Version: 0.1.5 API version: 0.3 - Copyright © 2018 Simon Stürz + Copyright © 2018 Simon Stürz Options: @@ -131,7 +131,7 @@ In order to get information about the server you can start the command with the --verbose Print more verbose. -# Server API +# Proxy server API Once a client connects to the proxy server, he must authenticate him self by passing the token received from the nymea-cloud mqtt connection request. @@ -208,6 +208,9 @@ If anything goes wrong, or the tunnel partner disconnects from the proxy, the se "status": "success" } + +# Proxy server + ## Authenticate the connection The first data a client **must** send to the proxy server is the authentication request. This request contains the `token` which will be verified agains the nymea-cloud infrastructure and a `nonce` which has to be uniq for each connection attempt and shared between the 2 clients. The `uuid` should be a persistant uuid for this client and the name should make clear which type of connection this is and which client is connecting. The name and uuid will be sent to the tunnel partner during the tunnel establishmend. @@ -270,76 +273,82 @@ Once the other client is here and ready, the server will send a notification to #### Response - "id": 1, - "params": { - "methods": { - "Authentication.Authenticate": { - "description": "Authenticate this connection. The returned AuthenticationError informs about the result. If the authentication was not successfull, the server will close the connection immediatly after sending the error response. The given id should be a unique id the other tunnel client can understand. Once the authentication was successfull, you can wait for the RemoteProxy.TunnelEstablished notification. If you send any data before getting this notification, the server will close the connection. If the tunnel client does not show up within 10 seconds, the server will close the connection.", - "params": { - "name": "String", - "o:nonce": "String", - "token": "String", - "uuid": "String" + { + "id": 1, + "params": { + "methods": { + "Authentication.Authenticate": { + "description": "Authenticate this connection. The returned AuthenticationError informs about the result. If the authentication was not successfull, the server will close the connection immediatly after sending the error response. The given id should be a unique id the other tunnel client can understand. Once the authentication was successfull, you can wait for the RemoteProxy.TunnelEstablished notification. If you send any data before getting this notification, the server will close the connection. If the tunnel client does not show up within 10 seconds, the server will close the connection.", + "params": { + "name": "String", + "o:nonce": "String", + "token": "String", + "uuid": "String" + }, + "returns": { + "authenticationError": "$ref:AuthenticationError" + } }, - "returns": { - "authenticationError": "$ref:AuthenticationError" + "RemoteProxy.Hello": { + "description": "Once connected to this server, a client can get information about the server by saying Hello. The response informs the client about this proxy server.", + "params": { + }, + "returns": { + "apiVersion": "String", + "name": "String", + "server": "String", + "version": "String" + } + }, + "RemoteProxy.Introspect": { + "description": "Introspect this API.", + "params": { + }, + "returns": { + "methods": "Object", + "notifications": "Object", + "types": "Object" + } } }, - "RemoteProxy.Hello": { - "description": "Once connected to this server, a client can get information about the server by saying Hello. The response informs the client about this proxy server.", - "params": { - }, - "returns": { - "apiVersion": "String", - "name": "String", - "server": "String", - "version": "String" + "notifications": { + "RemoteProxy.TunnelEstablished": { + "description": "Emitted whenever the tunnel has been established successfully. This is the last message from the remote proxy server! Any following data will be from the other tunnel client until the connection will be closed. The parameter contain some information about the other tunnel client.", + "params": { + "name": "String", + "uuid": "String" + } } }, - "RemoteProxy.Introspect": { - "description": "Introspect this API.", - "params": { - }, - "returns": { - "methods": "Object", - "notifications": "Object", - "types": "Object" - } + "types": { + "AuthenticationError": [ + "AuthenticationErrorNoError", + "AuthenticationErrorUnknown", + "AuthenticationErrorTimeout", + "AuthenticationErrorAborted", + "AuthenticationErrorAuthenticationFailed", + "AuthenticationErrorProxyError" + ], + "BasicType": [ + "Uuid", + "String", + "Int", + "UInt", + "Double", + "Bool", + "Variant", + "Object" + ] } }, - "notifications": { - "RemoteProxy.TunnelEstablished": { - "description": "Emitted whenever the tunnel has been established successfully. This is the last message from the remote proxy server! Any following data will be from the other tunnel client until the connection will be closed. The parameter contain some information about the other tunnel client.", - "params": { - "name": "String", - "uuid": "String" - } - } - }, - "types": { - "AuthenticationError": [ - "AuthenticationErrorNoError", - "AuthenticationErrorUnknown", - "AuthenticationErrorTimeout", - "AuthenticationErrorAborted", - "AuthenticationErrorAuthenticationFailed", - "AuthenticationErrorProxyError" - ], - "BasicType": [ - "Uuid", - "String", - "Int", - "UInt", - "Double", - "Bool", - "Variant", - "Object" - ] - } - }, - "status": "success" -} + "status": "success" + } + +# Proxy server + + + # Server monitor The server provides a live monitor interface on a local socket server. You can follow the monitor data with: @@ -358,7 +367,7 @@ There is also the package `nymea-remoteproxy-monitor` package and application wh Server version: 0.1.5 API version: 0.3 - Copyright © 2018 Simon Stürz + Copyright © 2018 Simon Stürz Options: @@ -384,7 +393,7 @@ The client allowes you to test the proxy server and create a dummy client for te Version: 0.1.5 API version: 0.3 - Copyright © 2018 Simon Stürz + Copyright © 2018 Simon Stürz Options: diff --git a/debian/control b/debian/control index 38af69e..7d6d42e 100644 --- a/debian/control +++ b/debian/control @@ -1,12 +1,13 @@ Source: nymea-remoteproxy Section: utils Priority: options -Maintainer: Simon Stürz +Maintainer: Simon Stürz Build-depends: debhelper (>= 0.0.0), libqt5websockets5-dev, libncurses5-dev, Standards-Version: 3.9.3 + Package: nymea-remoteproxy Architecture: any Depends: ${shlibs:Depends}, @@ -17,6 +18,7 @@ Suggests: nymea-remoteproxy-monitor (= ${binary:Version}) Description: The nymea remote proxy server The nymea remote proxy server + Package: libnymea-remoteproxy Architecture: any Section: libs @@ -25,6 +27,7 @@ Depends: ${shlibs:Depends}, Description: The nymea remote proxy server lib The nymea remote proxy server lib + Package: libnymea-remoteproxy-dev Architecture: any Section: libdevel @@ -34,15 +37,6 @@ Depends: ${shlibs:Depends}, Description: The nymea remote proxy server lib - development files The nymea remote proxy server lib - development files -Package: libnymea-remoteproxy-dbg -Architecture: any -Section: debug -Depends: ${shlibs:Depends}, - ${misc:Depends}, - libnymea-remoteproxy (= ${binary:Version}), -Description: The nymea remote proxy server lib - debug symbols - The nymea remote proxy server lib - debug symbols - Package: nymea-remoteproxy-client Architecture: any @@ -52,6 +46,7 @@ Depends: ${shlibs:Depends}, Description: The nymea remote proxy client for testing The nymea remote proxy client for testing + Package: libnymea-remoteproxyclient Architecture: any Section: libs @@ -60,6 +55,7 @@ Depends: ${shlibs:Depends}, Description: The nymea remote proxy server client lib The nymea remote proxy server client lib + Package: libnymea-remoteproxyclient-dev Architecture: any Section: libdevel @@ -69,14 +65,6 @@ Depends: ${shlibs:Depends}, Description: The nymea remote proxy server client lib - development files The nymea remote proxy server client lib - development files -Package: libnymea-remoteproxyclient-dbg -Architecture: any -Section: debug -Depends: ${shlibs:Depends}, - ${misc:Depends}, - libnymea-remoteproxyclient (= ${binary:Version}), -Description: The nymea remote proxy server client lib - debug symbols - The nymea remote proxy server client lib - debug symbols Package: nymea-remoteproxy-tests Architecture: any @@ -86,6 +74,7 @@ Depends: ${shlibs:Depends}, Description: The nymea remote proxy server tests The nymea remote proxy server tests + Package: nymea-remoteproxy-monitor Architecture: any Depends: ${shlibs:Depends}, diff --git a/debian/copyright b/debian/copyright index be434ed..6e741f3 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,10 +1,9 @@ Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: nymea-remoteproxy -Upstream-Contact: Simon Stürz -Copyright: 2018, guh GmbH -Download: http://www.github.com/guh/nymea-remoteproxy -Source: https://github.com/guh/nymea-remoteproxy.git - +Upstream-Contact: Simon Stürz +Copyright: 2018 - 2021, nymea GmbH +Download: http://www.github.com/nymea/nymea-remoteproxy +Source: https://github.com/nymea/nymea-remoteproxy.git License: GPL-3 On Debian systems, the complete text of the GNU General @@ -17,20 +16,24 @@ License: LGPL-3 Files: libnymea-remoteproxyclient/* License: LGPL-3 -Copyright: 2018 - 2010, nymea GmbH +Copyright: 2018 - 2021, nymea GmbH Files: libnymea-remoteproxy/* License: GPL-3 -Copyright: 2018 - 2010, nymea GmbH +Copyright: 2018 - 2021, nymea GmbH Files: client/* License: GPL-3 -Copyright: 2018 - 2010, nymea GmbH +Copyright: 2018 - 2021, nymea GmbH Files: monitor/* License: GPL-3 -Copyright: 2018 - 2010, nymea GmbH +Copyright: 2018 - 2021, nymea GmbH Files: server/* License: GPL-3 -Copyright: 2018 - 2010, nymea GmbH +Copyright: 2018 - 2021, nymea GmbH + +Files: tests/* +License: GPL-3 +Copyright: 2018 - 2021, nymea GmbH diff --git a/debian/nymea-remoteproxy-tests.install b/debian/nymea-remoteproxy-tests.install index 059a6b9..bec587f 100644 --- a/debian/nymea-remoteproxy-tests.install +++ b/debian/nymea-remoteproxy-tests.install @@ -1 +1,2 @@ -usr/bin/nymea-remoteproxy-tests-offline +usr/bin/nymea-remoteproxy-proxy-tests +usr/bin/nymea-remoteproxy-tunnelproxy-tests diff --git a/docs/remote-connection-basic-flow.png b/docs/remote-connection-basic-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..11b041a8aab98dc4185060cf891930eaf520b828 GIT binary patch literal 92607 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ⅇ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

PmX?Z51S6 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 literal 0 HcmV?d00001 diff --git a/docs/remote-connection-basic-flow.svg b/docs/remote-connection-basic-flow.svg new file mode 100644 index 0000000..f8a29ea --- /dev/null +++ b/docs/remote-connection-basic-flow.svg @@ -0,0 +1 @@ +title%20Remote%20tunnel%20proxy%0A%0Anymea-%3Eproxy%3A%20RegisterProxyTunnelServer(uuid%2C%20name)%0A%0Anote%20over%20proxy%3A%20Register%20the%20server%20using%20the%20uuid%0A%0Anymea%3C-proxy%3A%20Success%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%20ConnectProxyTunnelClient(uuid%2C%20name%2C%20serverUuid)%0A%0Anote%20over%20proxy%3A%20Search%20server%20using%20uuid%0A%0Anote%20over%20proxy%3A%20Server%3A%20Assign%20address%200x0001%20for%20this%20client%20socket%0A%0Aproxy-%3Enymea%3A%20SLIP%3A0x0000%3A%20ProxyTunnelClientConnected%20(address%3A%200x0001)%0A%0Aproxy%3C-nymea%3A%20SLIP%3A0x0000%3A%20AckProxyTunnelClient%20(address%3A%200x0001)%0A%0Aproxy-%3Eclient%3A%20ProxyTunnelEstablished(uuid%2C%20name%2C%20serverUuid)%0A%0Anote%20over%20proxy%2C%20client%3A%20Protocol%20from%20now%20on%20SLIP%20encoded%5CnThe%20proxy%20is%20client%200x0000%5CnThe%20connected%20server%20is%200xFFFF%0A%0Anote%20over%20proxy%2C%20client%3A%20SLIP%20encoded%20data%3A%202%20Bytes%20address%20%2B%20data%0A%0Anote%20over%20nymea%2C%20client%3A%20Connected%3A%20The%20client%20can%20now%20communicate%20with%20nymea%20directly.%0A%0Anote%20over%20nymea%2C%20client%3A%20nymea%20sends%20SLIP%20encoded%20data%20with%20address%200x0001%20-%3E%20client%0A%0Anote%20over%20nymea%2C%20client%3A%20client%20sends%20SLIP%20endcoded%20data%20with%20address%200xFFFF%20-%3E%20nymeaRemote tunnel proxynymeaproxyclientRegisterProxyTunnelServer(uuid, name)Register the server using the uuidSuccessProtocol from now on SLIPThe proxy is client 0x0000SLIP encoded data: 2 Bytes address + dataConnectProxyTunnelClient(uuid, name, serverUuid)Search server using uuidServer: Assign address 0x0001 for this client socketSLIP:0x0000: ProxyTunnelClientConnected (address: 0x0001)SLIP:0x0000: AckProxyTunnelClient (address: 0x0001)ProxyTunnelEstablished(uuid, name, serverUuid)Protocol from now on SLIP encodedThe proxy is client 0x0000The connected server is 0xFFFFSLIP encoded data: 2 Bytes address + dataConnected: The client can now communicate with nymea directly.nymea sends SLIP encoded data with address 0x0001 -> clientclient sends SLIP endcoded data with address 0xFFFF -> nymea \ No newline at end of file diff --git a/docs/remote-connection-basic-flow.txt b/docs/remote-connection-basic-flow.txt new file mode 100644 index 0000000..24bb29e --- /dev/null +++ b/docs/remote-connection-basic-flow.txt @@ -0,0 +1,33 @@ +title Remote tunnel proxy + +nymea->proxy: RegisterProxyTunnelServer(uuid, name) + +note over proxy: Register the server using the uuid + +nymea<-proxy: Success + +note over nymea, proxy: Protocol from now on SLIP\nThe proxy is client 0x0000 + +note over nymea, proxy: SLIP encoded data: 2 Bytes address + data + +proxy<-client: ConnectProxyTunnelClient(uuid, name, serverUuid) + +note over proxy: Search server using uuid + +note over proxy: Server: Assign address 0x0001 for this client socket + +proxy->nymea: SLIP:0x0000: ProxyTunnelClientConnected (address: 0x0001) + +proxy<-nymea: SLIP:0x0000: AckProxyTunnelClient (address: 0x0001) + +proxy->client: ProxyTunnelEstablished(uuid, name, serverUuid) + +note over proxy, client: Protocol from now on SLIP encoded\nThe proxy is client 0x0000\nThe connected server is 0xFFFF + +note over proxy, client: SLIP encoded data: 2 Bytes address + data + +note over nymea, client: Connected: The client can now communicate with nymea directly. + +note over nymea, client: nymea sends SLIP encoded data with address 0x0001 -> client + +note over nymea, client: client sends SLIP endcoded data with address 0xFFFF -> nymea diff --git a/libnymea-remoteproxy/engine.cpp b/libnymea-remoteproxy/engine.cpp index 82bee33..5cd1a8c 100644 --- a/libnymea-remoteproxy/engine.cpp +++ b/libnymea-remoteproxy/engine.cpp @@ -71,32 +71,62 @@ void Engine::start(ProxyConfiguration *configuration) // Make sure an authenticator was registered Q_ASSERT_X(m_authenticator != nullptr, "Engine", "There is no authenticator registerd."); + // Proxy + // ------------------------------------- m_proxyServer = new ProxyServer(this); - m_webSocketServer = new WebSocketServer(m_configuration->sslEnabled(), m_configuration->sslConfiguration(), this); - m_tcpSocketServer = new TcpSocketServer(m_configuration->sslEnabled(), m_configuration->sslConfiguration(), this); + m_webSocketServerProxy = new WebSocketServer(m_configuration->sslEnabled(), m_configuration->sslConfiguration(), this); + m_tcpSocketServerProxy = new TcpSocketServer(m_configuration->sslEnabled(), m_configuration->sslConfiguration(), this); // Configure websocket server QUrl websocketServerUrl; websocketServerUrl.setScheme(m_configuration->sslEnabled() ? "wss" : "ws"); - websocketServerUrl.setHost(m_configuration->webSocketServerHost().toString()); - websocketServerUrl.setPort(m_configuration->webSocketServerPort()); - m_webSocketServer->setServerUrl(websocketServerUrl); + websocketServerUrl.setHost(m_configuration->webSocketServerProxyHost().toString()); + websocketServerUrl.setPort(m_configuration->webSocketServerProxyPort()); + m_webSocketServerProxy->setServerUrl(websocketServerUrl); // Configure tcp socket server - QUrl tcpSocketServerUrl; - tcpSocketServerUrl.setScheme(m_configuration->sslEnabled() ? "ssl" : "tcp"); - tcpSocketServerUrl.setHost(m_configuration->tcpServerHost().toString()); - tcpSocketServerUrl.setPort(m_configuration->tcpServerPort()); - m_tcpSocketServer->setServerUrl(tcpSocketServerUrl); + QUrl tcpSocketServerProxyUrl; + tcpSocketServerProxyUrl.setScheme(m_configuration->sslEnabled() ? "ssl" : "tcp"); + tcpSocketServerProxyUrl.setHost(m_configuration->tcpServerHost().toString()); + tcpSocketServerProxyUrl.setPort(m_configuration->tcpServerPort()); + m_tcpSocketServerProxy->setServerUrl(tcpSocketServerProxyUrl); // Register the transport interfaces in the proxy server - m_proxyServer->registerTransportInterface(m_webSocketServer); - m_proxyServer->registerTransportInterface(m_tcpSocketServer); + m_proxyServer->registerTransportInterface(m_webSocketServerProxy); + m_proxyServer->registerTransportInterface(m_tcpSocketServerProxy); // Start the server - qCDebug(dcEngine()) << "Starting proxy server"; + qCDebug(dcEngine()) << "Starting the proxy servers..."; m_proxyServer->startServer(); + // Tunnel proxy + // ------------------------------------- + m_tunnelProxyManager = new TunnelProxyManager(this); + m_webSocketServerTunnelProxy = new WebSocketServer(m_configuration->sslEnabled(), m_configuration->sslConfiguration(), this); + m_tcpSocketServerTunnelProxy = new TcpSocketServer(m_configuration->sslEnabled(), m_configuration->sslConfiguration(), this); + + // Configure websocket server + QUrl websocketServerTunnelProxyUrl; + websocketServerTunnelProxyUrl.setScheme(m_configuration->sslEnabled() ? "wss" : "ws"); + websocketServerTunnelProxyUrl.setHost(m_configuration->webSocketServerTunnelProxyHost().toString()); + websocketServerTunnelProxyUrl.setPort(m_configuration->webSocketServerTunnelProxyPort()); + m_webSocketServerTunnelProxy->setServerUrl(websocketServerTunnelProxyUrl); + + // Configure tcp socket server + QUrl tcpSocketServerTunnelProxyUrl; + tcpSocketServerTunnelProxyUrl.setScheme(m_configuration->sslEnabled() ? "ssl" : "tcp"); + tcpSocketServerTunnelProxyUrl.setHost(m_configuration->tcpServerTunnelProxyHost().toString()); + tcpSocketServerTunnelProxyUrl.setPort(m_configuration->tcpServerTunnelProxyPort()); + m_tcpSocketServerTunnelProxy->setServerUrl(tcpSocketServerTunnelProxyUrl); + + // Register the transport interfaces in the proxy server + m_tunnelProxyManager->registerTransportInterface(m_webSocketServerTunnelProxy); + m_tunnelProxyManager->registerTransportInterface(m_tcpSocketServerTunnelProxy); + + // Start the server + qCDebug(dcEngine()) << "Starting the tunnel proxy manager..."; + m_tunnelProxyManager->startServer(); + // Start the monitor server m_monitorServer = new MonitorServer(configuration->monitorSocketFileName(), this); m_monitorServer->startServer(); @@ -165,14 +195,29 @@ ProxyServer *Engine::proxyServer() const return m_proxyServer; } -TcpSocketServer *Engine::tcpSocketServer() const +TunnelProxyManager *Engine::tunnelProxyManager() const { - return m_tcpSocketServer; + return m_tunnelProxyManager; } -WebSocketServer *Engine::webSocketServer() const +TcpSocketServer *Engine::tcpSocketServerProxy() const { - return m_webSocketServer; + return m_tcpSocketServerProxy; +} + +WebSocketServer *Engine::webSocketServerProxy() const +{ + return m_webSocketServerProxy; +} + +TcpSocketServer *Engine::tcpSocketServerTunnelProxy() const +{ + return m_tcpSocketServerTunnelProxy; +} + +WebSocketServer *Engine::webSocketServerTunnelProxy() const +{ + return m_webSocketServerTunnelProxy; } MonitorServer *Engine::monitorServer() const @@ -188,7 +233,7 @@ LogEngine *Engine::logEngine() const Engine::Engine(QObject *parent) : QObject(parent) { - m_lastTimeStamp = QDateTime::currentDateTime().toMSecsSinceEpoch(); + m_lastTimeStamp = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(); m_timer = new QTimer(this); m_timer->setSingleShot(false); @@ -216,7 +261,7 @@ QVariantMap Engine::createServerStatistic() void Engine::onTimerTick() { - qint64 timestamp = QDateTime::currentDateTime().toMSecsSinceEpoch(); + qint64 timestamp = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(); qint64 deltaTime = timestamp - m_lastTimeStamp; m_lastTimeStamp = timestamp; @@ -249,9 +294,9 @@ void Engine::clean() m_proxyServer = nullptr; } - if (m_webSocketServer) { - delete m_webSocketServer; - m_webSocketServer = nullptr; + if (m_webSocketServerProxy) { + delete m_webSocketServerProxy; + m_webSocketServerProxy = nullptr; } if (m_configuration) { @@ -265,6 +310,9 @@ void Engine::setRunning(bool running) if (m_running == running) return; + if (m_proxyServer) + m_proxyServer->setRunning(running); + qCDebug(dcEngine()) << "Engine is" << (running ? "now running." : "not running any more."); if (running) { diff --git a/libnymea-remoteproxy/engine.h b/libnymea-remoteproxy/engine.h index 849ae6f..9fc5264 100644 --- a/libnymea-remoteproxy/engine.h +++ b/libnymea-remoteproxy/engine.h @@ -36,12 +36,14 @@ #include #include "logengine.h" -#include "proxyserver.h" -#include "monitorserver.h" -#include "tcpsocketserver.h" -#include "websocketserver.h" +#include "proxy/proxyserver.h" #include "proxyconfiguration.h" +#include "server/monitorserver.h" +#include "server/jsonrpcserver.h" +#include "server/tcpsocketserver.h" +#include "server/websocketserver.h" #include "authentication/authenticator.h" +#include "tunnelproxy/tunnelproxymanager.h" namespace remoteproxy { @@ -67,9 +69,17 @@ public: ProxyConfiguration *configuration() const; Authenticator *authenticator() const; + ProxyServer *proxyServer() const; - TcpSocketServer *tcpSocketServer() const; - WebSocketServer *webSocketServer() const; + TunnelProxyManager *tunnelProxyManager() const; + + TcpSocketServer *tcpSocketServerProxy() const; + WebSocketServer *webSocketServerProxy() const; + + TcpSocketServer *tcpSocketServerTunnelProxy() const; + WebSocketServer *webSocketServerTunnelProxy() const; + + MonitorServer *monitorServer() const; LogEngine *logEngine() const; @@ -89,9 +99,16 @@ private: ProxyConfiguration *m_configuration = nullptr; Authenticator *m_authenticator = nullptr; + ProxyServer *m_proxyServer = nullptr; - TcpSocketServer *m_tcpSocketServer = nullptr; - WebSocketServer *m_webSocketServer = nullptr; + TunnelProxyManager *m_tunnelProxyManager = nullptr; + + TcpSocketServer *m_tcpSocketServerProxy = nullptr; + WebSocketServer *m_webSocketServerProxy = nullptr; + + TcpSocketServer *m_tcpSocketServerTunnelProxy = nullptr; + WebSocketServer *m_webSocketServerTunnelProxy = nullptr; + MonitorServer *m_monitorServer = nullptr; LogEngine *m_logEngine = nullptr; diff --git a/libnymea-remoteproxy/jsonrpc/authenticationhandler.cpp b/libnymea-remoteproxy/jsonrpc/authenticationhandler.cpp index 57ab1fb..5e43cb1 100644 --- a/libnymea-remoteproxy/jsonrpc/authenticationhandler.cpp +++ b/libnymea-remoteproxy/jsonrpc/authenticationhandler.cpp @@ -106,7 +106,7 @@ void AuthenticationHandler::onAuthenticationFinished() jsonReply->setData(errorToReply(authenticationReply->error())); } - jsonReply->finished(); + emit jsonReply->finished(); } } diff --git a/libnymea-remoteproxy/jsonrpc/jsontypes.cpp b/libnymea-remoteproxy/jsonrpc/jsontypes.cpp index ce350ec..b54bf48 100644 --- a/libnymea-remoteproxy/jsonrpc/jsontypes.cpp +++ b/libnymea-remoteproxy/jsonrpc/jsontypes.cpp @@ -40,11 +40,11 @@ QString JsonTypes::s_lastError; // Types QVariantList JsonTypes::s_basicType; QVariantList JsonTypes::s_authenticationError; +QVariantList JsonTypes::s_tunnelProxyError; // Objects - QVariantMap JsonTypes::allTypes() { QVariantMap allTypes; @@ -52,6 +52,7 @@ QVariantMap JsonTypes::allTypes() // Enums allTypes.insert("BasicType", basicType()); allTypes.insert("AuthenticationError", authenticationError()); + allTypes.insert("TunnelProxyError", tunnelProxyError()); // Types @@ -63,6 +64,7 @@ void JsonTypes::init() // Declare types s_basicType = enumToStrings(JsonTypes::staticMetaObject, "BasicType"); s_authenticationError = enumToStrings(Authenticator::staticMetaObject, "AuthenticationError"); + s_tunnelProxyError = enumToStrings(TunnelProxyManager::staticMetaObject, "Error"); s_initialized = true; } @@ -124,10 +126,17 @@ QPair JsonTypes::validateVariant(const QVariant &templateVariant, qCWarning(dcJsonRpc()) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(authenticationErrorRef()); return result; } + } else if (refName == tunnelProxyErrorRef()) { + QPair result = validateEnum(s_tunnelProxyError, variant); + if (!result.first) { + qCWarning(dcJsonRpc()) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(tunnelProxyErrorRef()); + return result; + } } else { Q_ASSERT_X(false, "JsonTypes", QString("Unhandled ref: %1").arg(refName).toLatin1().data()); return report(false, QString("Unhandled ref %1. Server implementation incomplete.").arg(refName)); } + } else { QPair result = JsonTypes::validateProperty(templateVariant, variant); if (!result.first) { diff --git a/libnymea-remoteproxy/jsonrpc/jsontypes.h b/libnymea-remoteproxy/jsonrpc/jsontypes.h index 3ccd68a..4b85732 100644 --- a/libnymea-remoteproxy/jsonrpc/jsontypes.h +++ b/libnymea-remoteproxy/jsonrpc/jsontypes.h @@ -33,6 +33,7 @@ #include #include +#include "tunnelproxy/tunnelproxymanager.h" #include "authentication/authenticator.h" namespace remoteproxy { @@ -90,6 +91,7 @@ public: // Declare types DECLARE_TYPE(basicType, "BasicType", JsonTypes, BasicType) DECLARE_TYPE(authenticationError, "AuthenticationError", Authenticator, AuthenticationError) + DECLARE_TYPE(tunnelProxyError, "TunnelProxyError", TunnelProxyManager, Error) // Declare objects diff --git a/libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.cpp b/libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.cpp new file mode 100644 index 0000000..d896287 --- /dev/null +++ b/libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.cpp @@ -0,0 +1,72 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "tunnelproxyhandler.h" + +#include "engine.h" +#include "jsontypes.h" +#include "loggingcategories.h" + +#include "tunnelproxy/tunnelproxymanager.h" + +namespace remoteproxy { + +TunnelProxyHandler::TunnelProxyHandler(QObject *parent) : JsonHandler(parent) +{ + // Methods + QVariantMap params; QVariantMap returns; + + params.clear(); returns.clear(); + setDescription("RegisterServer", "Register a new TunnelProxy server on this instance. Multiple TunnelProxy clients can be connected to the registered server on success."); + params.insert("uuid", JsonTypes::basicTypeToString(JsonTypes::String)); + params.insert("name", JsonTypes::basicTypeToString(JsonTypes::String)); + setParams("RegisterServer", params); + returns.insert("tunnelProxyError", JsonTypes::tunnelProxyErrorRef()); + setReturns("RegisterServer", returns); + +} + +QString TunnelProxyHandler::name() const +{ + return "TunnelProxy"; +} + +JsonReply *TunnelProxyHandler::RegisterServer(const QVariantMap ¶ms, ProxyClient *proxyClient) +{ + qCDebug(dcJsonRpc()) << name() << "register server" << params << proxyClient; + QUuid serverUuid = params.value("uuid").toUuid(); + QString serverName = params.value("name").toString(); + + TunnelProxyManager::Error error = Engine::instance()->tunnelProxyManager()->registerServer(proxyClient->clientId(), serverUuid, serverName); + + QVariantMap response; + response.insert("tunnelProxyError", JsonTypes::tunnelProxyErrorToString(error)); + + return createReply("RegisterServer", response); +} + +} diff --git a/libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.h b/libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.h new file mode 100644 index 0000000..9fb29f4 --- /dev/null +++ b/libnymea-remoteproxy/jsonrpc/tunnelproxyhandler.h @@ -0,0 +1,55 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef TUNNELPROXYHANDLER_H +#define TUNNELPROXYHANDLER_H + +#include + +#include "jsonhandler.h" + +namespace remoteproxy { + +class TunnelProxyHandler : public JsonHandler +{ + Q_OBJECT +public: + explicit TunnelProxyHandler(QObject *parent = nullptr); + ~TunnelProxyHandler() override = default; + + QString name() const override; + + Q_INVOKABLE JsonReply *RegisterServer(const QVariantMap ¶ms, ProxyClient *proxyClient); + +signals: + + +}; + +} + +#endif // TUNNELPROXYHANDLER_H diff --git a/libnymea-remoteproxy/libnymea-remoteproxy.pro b/libnymea-remoteproxy/libnymea-remoteproxy.pro index 373a8e8..b5a179d 100644 --- a/libnymea-remoteproxy/libnymea-remoteproxy.pro +++ b/libnymea-remoteproxy/libnymea-remoteproxy.pro @@ -6,19 +6,22 @@ TARGET = nymea-remoteproxy HEADERS += \ engine.h \ loggingcategories.h \ - tcpsocketserver.h \ - transportinterface.h \ - websocketserver.h \ + tunnelproxy/tunnelproxymanager.h \ + tunnelproxy/tunnelproxyserver.h \ + server/tcpsocketserver.h \ + server/transportinterface.h \ + server/websocketserver.h \ + server/jsonrpcserver.h \ + server/monitorserver.h \ proxyclient.h \ - proxyserver.h \ - monitorserver.h \ + proxy/proxyserver.h \ + proxy/tunnelconnection.h \ proxyconfiguration.h \ - tunnelconnection.h \ - jsonrpcserver.h \ jsonrpc/jsonhandler.h \ jsonrpc/jsonreply.h \ jsonrpc/jsontypes.h \ jsonrpc/authenticationhandler.h \ + jsonrpc/tunnelproxyhandler.h \ authentication/authenticator.h \ authentication/authenticationreply.h \ authentication/dummy/dummyauthenticator.h \ @@ -32,19 +35,22 @@ HEADERS += \ SOURCES += \ engine.cpp \ loggingcategories.cpp \ - tcpsocketserver.cpp \ - transportinterface.cpp \ - websocketserver.cpp \ + tunnelproxy/tunnelproxymanager.cpp \ + tunnelproxy/tunnelproxyserver.cpp \ + server/tcpsocketserver.cpp \ + server/transportinterface.cpp \ + server/websocketserver.cpp \ + server/jsonrpcserver.cpp \ + server/monitorserver.cpp \ proxyclient.cpp \ - proxyserver.cpp \ - monitorserver.cpp \ + proxy/proxyserver.cpp \ + proxy/tunnelconnection.cpp \ proxyconfiguration.cpp \ - tunnelconnection.cpp \ - jsonrpcserver.cpp \ jsonrpc/jsonhandler.cpp \ jsonrpc/jsonreply.cpp \ jsonrpc/jsontypes.cpp \ jsonrpc/authenticationhandler.cpp \ + jsonrpc/tunnelproxyhandler.cpp \ authentication/authenticator.cpp \ authentication/authenticationreply.cpp \ authentication/dummy/dummyauthenticator.cpp \ @@ -57,7 +63,7 @@ SOURCES += \ # install header file with relative subdirectory -for(header, HEADERS) { +for (header, HEADERS) { path = $$[QT_INSTALL_PREFIX]/include/nymea-remoteproxy/$${dirname(header)} eval(headers_$${path}.files += $${header}) eval(headers_$${path}.path = $${path}) diff --git a/libnymea-remoteproxy/logengine.cpp b/libnymea-remoteproxy/logengine.cpp index 1a5d232..e547ff2 100644 --- a/libnymea-remoteproxy/logengine.cpp +++ b/libnymea-remoteproxy/logengine.cpp @@ -34,7 +34,7 @@ namespace remoteproxy { LogEngine::LogEngine(QObject *parent) : QObject(parent) { - m_currentDay = QDateTime::currentDateTime().date().day(); + m_currentDay = QDateTime::currentDateTimeUtc().date().day(); m_tunnelsFileName = "/var/log/nymea-remoteproxy-tunnels"; m_statisticsFileName = "/var/log/nymea-remoteproxy-statistics"; } @@ -79,9 +79,9 @@ void LogEngine::logStatistics(int tunnelCount, int connectionCount, int troughpu textStream << logString.join(" ") << endl; // Check if we have to rotate the logfile - if (m_currentDay != QDateTime::currentDateTime().date().day()) { + if (m_currentDay != QDateTime::currentDateTimeUtc().date().day()) { // Day changed - m_currentDay = QDateTime::currentDateTime().date().day(); + m_currentDay = QDateTime::currentDateTimeUtc().date().day(); rotateLogs(); } } @@ -98,7 +98,7 @@ void LogEngine::rotateLogs() m_statisticsFile.close(); // Rename the current files - QString postfix = "-" + QDateTime::currentDateTime().toString("yyyyMMddhhmmss") + ".log"; + QString postfix = "-" + QDateTime::currentDateTimeUtc().toString("yyyyMMddhhmmss") + ".log"; m_tunnelsFile.rename(m_tunnelsFileName + postfix); qCDebug(dcApplication()) << "Rotate logfile" << m_tunnelsFile.fileName(); @@ -120,7 +120,7 @@ void LogEngine::rotateLogs() QString LogEngine::createTimestamp() { - return QString::number(QDateTime::currentDateTime().toTime_t()); + return QString::number(QDateTime::currentDateTimeUtc().toTime_t()); } void LogEngine::enable() diff --git a/libnymea-remoteproxy/logengine.h b/libnymea-remoteproxy/logengine.h index 73c4014..5acf8b3 100644 --- a/libnymea-remoteproxy/logengine.h +++ b/libnymea-remoteproxy/logengine.h @@ -31,7 +31,7 @@ #include #include -#include "tunnelconnection.h" +#include "proxy/tunnelconnection.h" namespace remoteproxy { diff --git a/libnymea-remoteproxy/loggingcategories.cpp b/libnymea-remoteproxy/loggingcategories.cpp index 5bf36b7..6276313 100644 --- a/libnymea-remoteproxy/loggingcategories.cpp +++ b/libnymea-remoteproxy/loggingcategories.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2021, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -39,6 +39,9 @@ Q_LOGGING_CATEGORY(dcWebSocketServerTraffic, "WebSocketServerTraffic") Q_LOGGING_CATEGORY(dcAuthentication, "Authentication") Q_LOGGING_CATEGORY(dcAuthenticationProcess, "AuthenticationProcess") Q_LOGGING_CATEGORY(dcProxyServer, "ProxyServer") +Q_LOGGING_CATEGORY(dcTunnelProxyManager, "TunnelProxyManager") +Q_LOGGING_CATEGORY(dcProxyTunnelClient, "ProxyTunnelClient") +Q_LOGGING_CATEGORY(dcTunnelProxyServer, "TunnelProxyServer") Q_LOGGING_CATEGORY(dcProxyServerTraffic, "ProxyServerTraffic") Q_LOGGING_CATEGORY(dcMonitorServer, "MonitorServer") Q_LOGGING_CATEGORY(dcAwsCredentialsProvider, "AwsCredentialsProvider") diff --git a/libnymea-remoteproxy/loggingcategories.h b/libnymea-remoteproxy/loggingcategories.h index f9b4464..2c4addd 100644 --- a/libnymea-remoteproxy/loggingcategories.h +++ b/libnymea-remoteproxy/loggingcategories.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2021, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -43,6 +43,9 @@ Q_DECLARE_LOGGING_CATEGORY(dcTcpSocketServerTraffic) Q_DECLARE_LOGGING_CATEGORY(dcAuthentication) Q_DECLARE_LOGGING_CATEGORY(dcAuthenticationProcess) Q_DECLARE_LOGGING_CATEGORY(dcProxyServer) +Q_DECLARE_LOGGING_CATEGORY(dcTunnelProxyManager) +Q_DECLARE_LOGGING_CATEGORY(dcProxyTunnelClient) +Q_DECLARE_LOGGING_CATEGORY(dcTunnelProxyServer) Q_DECLARE_LOGGING_CATEGORY(dcProxyServerTraffic) Q_DECLARE_LOGGING_CATEGORY(dcMonitorServer) Q_DECLARE_LOGGING_CATEGORY(dcAwsCredentialsProvider) diff --git a/libnymea-remoteproxy/proxyserver.cpp b/libnymea-remoteproxy/proxy/proxyserver.cpp similarity index 97% rename from libnymea-remoteproxy/proxyserver.cpp rename to libnymea-remoteproxy/proxy/proxyserver.cpp index 6048114..6e837f0 100644 --- a/libnymea-remoteproxy/proxyserver.cpp +++ b/libnymea-remoteproxy/proxy/proxyserver.cpp @@ -36,10 +36,13 @@ namespace remoteproxy { -ProxyServer::ProxyServer(QObject *parent) : QObject(parent) +ProxyServer::ProxyServer(QObject *parent) : + QObject(parent) { qRegisterMetaType("ProxyClient *"); m_jsonRpcServer = new JsonRpcServer(this); + m_jsonRpcServer->registerHandler(m_jsonRpcServer); + m_jsonRpcServer->registerHandler(new AuthenticationHandler(this)); loadStatistics(); } @@ -120,7 +123,7 @@ void ProxyServer::setRunning(bool running) if (m_running == running) return; - qCDebug(dcProxyServer()) << "The proxy server is now up and running"; + qCDebug(dcProxyServer()) << "The proxy server is" << (running ? "up and running." : "not running any more."); m_running = running; emit runningChanged(); } @@ -196,13 +199,13 @@ void ProxyServer::establishTunnel(ProxyClient *firstClient, ProxyClient *secondC Q_ARG(QString, m_jsonRpcServer->name()), Q_ARG(QString, "TunnelEstablished"), Q_ARG(QVariantMap, notificationParamsFirst), - Q_ARG(ProxyClient *, tunnel.clientOne())); + Q_ARG(ProxyClient*, tunnel.clientOne())); QMetaObject::invokeMethod(m_jsonRpcServer, QString("sendNotification").toLatin1().data(), Qt::QueuedConnection, Q_ARG(QString, m_jsonRpcServer->name()), Q_ARG(QString, "TunnelEstablished"), Q_ARG(QVariantMap, notificationParamsSecond), - Q_ARG(ProxyClient *, tunnel.clientTwo())); + Q_ARG(ProxyClient*, tunnel.clientTwo())); } void ProxyServer::onClientConnected(const QUuid &clientId, const QHostAddress &address) diff --git a/libnymea-remoteproxy/proxyserver.h b/libnymea-remoteproxy/proxy/proxyserver.h similarity index 97% rename from libnymea-remoteproxy/proxyserver.h rename to libnymea-remoteproxy/proxy/proxyserver.h index 952d3d7..edc5ba2 100644 --- a/libnymea-remoteproxy/proxyserver.h +++ b/libnymea-remoteproxy/proxy/proxyserver.h @@ -33,9 +33,9 @@ #include #include "proxyclient.h" -#include "jsonrpcserver.h" #include "tunnelconnection.h" -#include "transportinterface.h" +#include "server/jsonrpcserver.h" +#include "server/transportinterface.h" namespace remoteproxy { @@ -47,6 +47,8 @@ public: ~ProxyServer(); bool running() const; + void setRunning(bool running); + void registerTransportInterface(TransportInterface *interface); QVariantMap currentStatistics(); @@ -78,10 +80,6 @@ private: int m_totalTunnelCount = 0; int m_totalTraffic = 0; - - // Set private properties - void setRunning(bool running); - void loadStatistics(); void saveStatistics(); diff --git a/libnymea-remoteproxy/tunnelconnection.cpp b/libnymea-remoteproxy/proxy/tunnelconnection.cpp similarity index 97% rename from libnymea-remoteproxy/tunnelconnection.cpp rename to libnymea-remoteproxy/proxy/tunnelconnection.cpp index 21aba8b..d097d9e 100644 --- a/libnymea-remoteproxy/tunnelconnection.cpp +++ b/libnymea-remoteproxy/proxy/tunnelconnection.cpp @@ -35,7 +35,7 @@ TunnelConnection::TunnelConnection(ProxyClient *clientOne, ProxyClient *clientTw m_clientOne(clientOne), m_clientTwo(clientTwo) { - m_creationTimeStamp = QDateTime::currentDateTime().toTime_t(); + m_creationTimeStamp = QDateTime::currentDateTimeUtc().toTime_t(); } QString TunnelConnection::token() const diff --git a/libnymea-remoteproxy/tunnelconnection.h b/libnymea-remoteproxy/proxy/tunnelconnection.h similarity index 100% rename from libnymea-remoteproxy/tunnelconnection.h rename to libnymea-remoteproxy/proxy/tunnelconnection.h diff --git a/libnymea-remoteproxy/proxyclient.cpp b/libnymea-remoteproxy/proxyclient.cpp index 0fc0bfc..35487ae 100644 --- a/libnymea-remoteproxy/proxyclient.cpp +++ b/libnymea-remoteproxy/proxyclient.cpp @@ -39,7 +39,7 @@ ProxyClient::ProxyClient(TransportInterface *interface, const QUuid &clientId, c m_clientId(clientId), m_peerAddress(address) { - m_creationTimeStamp = QDateTime::currentDateTime().toTime_t(); + m_creationTimeStamp = QDateTime::currentDateTimeUtc().toTime_t(); m_timer = new QTimer(this); connect(m_timer, &QTimer::timeout, this, &ProxyClient::timeoutOccured); diff --git a/libnymea-remoteproxy/proxyclient.h b/libnymea-remoteproxy/proxyclient.h index 492a429..996a49b 100644 --- a/libnymea-remoteproxy/proxyclient.h +++ b/libnymea-remoteproxy/proxyclient.h @@ -34,7 +34,7 @@ #include #include -#include "transportinterface.h" +#include "server/transportinterface.h" namespace remoteproxy { diff --git a/libnymea-remoteproxy/proxyconfiguration.cpp b/libnymea-remoteproxy/proxyconfiguration.cpp index 5d88dc0..d876c21 100644 --- a/libnymea-remoteproxy/proxyconfiguration.cpp +++ b/libnymea-remoteproxy/proxyconfiguration.cpp @@ -302,24 +302,24 @@ QSslConfiguration ProxyConfiguration::sslConfiguration() const return m_sslConfiguration; } -QHostAddress ProxyConfiguration::webSocketServerHost() const +QHostAddress ProxyConfiguration::webSocketServerProxyHost() const { - return m_webSocketServerHost; + return m_webSocketServerProxyHost; } void ProxyConfiguration::setWebSocketServerHost(const QHostAddress &address) { - m_webSocketServerHost = address; + m_webSocketServerProxyHost = address; } -quint16 ProxyConfiguration::webSocketServerPort() const +quint16 ProxyConfiguration::webSocketServerProxyPort() const { - return m_webSocketServerPort; + return m_webSocketServerProxyPort; } void ProxyConfiguration::setWebSocketServerPort(quint16 port) { - m_webSocketServerPort = port; + m_webSocketServerProxyPort = port; } QHostAddress ProxyConfiguration::tcpServerHost() const @@ -342,6 +342,46 @@ void ProxyConfiguration::setTcpServerPort(quint16 port) m_tcpServerPort = port; } +QHostAddress ProxyConfiguration::webSocketServerTunnelProxyHost() const +{ + return m_webSocketServerTunnelProxyHost; +} + +void ProxyConfiguration::setWebSocketServerTunnelProxyHost(const QHostAddress &address) +{ + m_webSocketServerTunnelProxyHost = address; +} + +quint16 ProxyConfiguration::webSocketServerTunnelProxyPort() const +{ + return m_webSocketServerTunnelProxyPort; +} + +void ProxyConfiguration::setWebSocketServerTunnelProxyPort(quint16 port) +{ + m_webSocketServerTunnelProxyPort = port; +} + +QHostAddress ProxyConfiguration::tcpServerTunnelProxyHost() const +{ + return m_tcpServerTunnelProxyHost; +} + +void ProxyConfiguration::setTcpServerTunnelProxyHost(const QHostAddress &address) +{ + m_tcpServerTunnelProxyHost = address; +} + +quint16 ProxyConfiguration::tcpServerTunnelProxyPort() const +{ + return m_tcpServerTunnelProxyPort; +} + +void ProxyConfiguration::setTcpServerTunnelProxyPort(quint16 port) +{ + m_tcpServerTunnelProxyPort = port; +} + QDebug operator<<(QDebug debug, ProxyConfiguration *configuration) { debug.nospace() << endl << "========== ProxyConfiguration ==========" << endl; @@ -380,12 +420,18 @@ QDebug operator<<(QDebug debug, ProxyConfiguration *configuration) debug.nospace() << " Locality name:" << configuration->sslConfiguration().localCertificate().issuerInfo(QSslCertificate::LocalityName) << endl; debug.nospace() << " State/Province:" << configuration->sslConfiguration().localCertificate().issuerInfo(QSslCertificate::StateOrProvinceName) << endl; debug.nospace() << " Email address:" << configuration->sslConfiguration().localCertificate().issuerInfo(QSslCertificate::EmailAddress) << endl; - debug.nospace() << "WebSocketServer configuration" << endl; - debug.nospace() << " - Host:" << configuration->webSocketServerHost().toString() << endl; - debug.nospace() << " - Port:" << configuration->webSocketServerPort() << endl; - debug.nospace() << "TcpServer" << endl; + debug.nospace() << "WebSocketServer Proxy" << endl; + debug.nospace() << " - Host:" << configuration->webSocketServerProxyHost().toString() << endl; + debug.nospace() << " - Port:" << configuration->webSocketServerProxyPort() << endl; + debug.nospace() << "TcpServer Proxy" << endl; debug.nospace() << " - Host:" << configuration->tcpServerHost().toString() << endl; debug.nospace() << " - Port:" << configuration->tcpServerPort() << endl; + debug.nospace() << "WebSocketServer TunnelProxy" << endl; + debug.nospace() << " - Host:" << configuration->webSocketServerTunnelProxyHost().toString() << endl; + debug.nospace() << " - Port:" << configuration->webSocketServerTunnelProxyPort() << endl; + debug.nospace() << "TcpServer TunnelProxy" << endl; + debug.nospace() << " - Host:" << configuration->tcpServerTunnelProxyHost().toString() << endl; + debug.nospace() << " - Port:" << configuration->tcpServerTunnelProxyPort() << endl; debug.nospace() << "========== ProxyConfiguration =========="; return debug; } diff --git a/libnymea-remoteproxy/proxyconfiguration.h b/libnymea-remoteproxy/proxyconfiguration.h index 2262761..ee61d05 100644 --- a/libnymea-remoteproxy/proxyconfiguration.h +++ b/libnymea-remoteproxy/proxyconfiguration.h @@ -100,10 +100,10 @@ public: QSslConfiguration sslConfiguration() const; // WebSocketServer - QHostAddress webSocketServerHost() const; + QHostAddress webSocketServerProxyHost() const; void setWebSocketServerHost(const QHostAddress &address); - quint16 webSocketServerPort() const; + quint16 webSocketServerProxyPort() const; void setWebSocketServerPort(quint16 port); // TcpServer @@ -113,6 +113,21 @@ public: quint16 tcpServerPort() const; void setTcpServerPort(quint16 port); + // WebSocketServer (tunnel) + QHostAddress webSocketServerTunnelProxyHost() const; + void setWebSocketServerTunnelProxyHost(const QHostAddress &address); + + quint16 webSocketServerTunnelProxyPort() const; + void setWebSocketServerTunnelProxyPort(quint16 port); + + // TcpServer (tunnel) + QHostAddress tcpServerTunnelProxyHost() const; + void setTcpServerTunnelProxyHost(const QHostAddress &address); + + quint16 tcpServerTunnelProxyPort() const; + void setTcpServerTunnelProxyPort(quint16 port); + + private: // ProxyServer QString m_fileName; @@ -139,14 +154,23 @@ private: QString m_sslCertificateChainFileName; QSslConfiguration m_sslConfiguration; - // WebSocketServer - QHostAddress m_webSocketServerHost = QHostAddress::LocalHost; - quint16 m_webSocketServerPort = 1212; + // WebSocketServer (proxy) + QHostAddress m_webSocketServerProxyHost = QHostAddress::LocalHost; + quint16 m_webSocketServerProxyPort = 1212; - // TcpServer + // TcpServer (proxy) QHostAddress m_tcpServerHost = QHostAddress::LocalHost; quint16 m_tcpServerPort = 1213; + // WebSocketServer (proxy) + QHostAddress m_webSocketServerTunnelProxyHost = QHostAddress::LocalHost; + quint16 m_webSocketServerTunnelProxyPort = 2212; + + // TcpServer (proxy) + QHostAddress m_tcpServerTunnelProxyHost = QHostAddress::LocalHost; + quint16 m_tcpServerTunnelProxyPort = 2213; + + }; QDebug operator<< (QDebug debug, ProxyConfiguration *configuration); diff --git a/libnymea-remoteproxy/jsonrpcserver.cpp b/libnymea-remoteproxy/server/jsonrpcserver.cpp similarity index 98% rename from libnymea-remoteproxy/jsonrpcserver.cpp rename to libnymea-remoteproxy/server/jsonrpcserver.cpp index 6753967..2bbf9c5 100644 --- a/libnymea-remoteproxy/jsonrpcserver.cpp +++ b/libnymea-remoteproxy/server/jsonrpcserver.cpp @@ -70,7 +70,6 @@ JsonRpcServer::JsonRpcServer(QObject *parent) : params.insert("name", JsonTypes::basicTypeToString(JsonTypes::String)); setParams("TunnelEstablished", params); - QMetaObject::invokeMethod(this, "setup", Qt::QueuedConnection); } JsonRpcServer::~JsonRpcServer() @@ -252,12 +251,6 @@ void JsonRpcServer::processDataPackage(ProxyClient *proxyClient, const QByteArra } } -void JsonRpcServer::setup() -{ - registerHandler(this); - registerHandler(new AuthenticationHandler(this)); -} - void JsonRpcServer::asyncReplyFinished() { JsonReply *reply = static_cast(sender()); diff --git a/libnymea-remoteproxy/jsonrpcserver.h b/libnymea-remoteproxy/server/jsonrpcserver.h similarity index 99% rename from libnymea-remoteproxy/jsonrpcserver.h rename to libnymea-remoteproxy/server/jsonrpcserver.h index 19249da..e6f1aba 100644 --- a/libnymea-remoteproxy/jsonrpcserver.h +++ b/libnymea-remoteproxy/server/jsonrpcserver.h @@ -51,6 +51,9 @@ public: Q_INVOKABLE JsonReply *Hello(const QVariantMap ¶ms, ProxyClient *proxyClient = nullptr) const; Q_INVOKABLE JsonReply *Introspect(const QVariantMap ¶ms, ProxyClient *proxyClient = nullptr) const; + void registerHandler(JsonHandler *handler); + void unregisterHandler(JsonHandler *handler); + signals: void TunnelEstablished(const QVariantMap ¶ms); @@ -66,12 +69,9 @@ private: QString formatAssertion(const QString &targetNamespace, const QString &method, JsonHandler *handler, const QVariantMap &data) const; - void registerHandler(JsonHandler *handler); - void unregisterHandler(JsonHandler *handler); void processDataPackage(ProxyClient *proxyClient, const QByteArray &data); private slots: - void setup(); void asyncReplyFinished(); public slots: diff --git a/libnymea-remoteproxy/monitorserver.cpp b/libnymea-remoteproxy/server/monitorserver.cpp similarity index 100% rename from libnymea-remoteproxy/monitorserver.cpp rename to libnymea-remoteproxy/server/monitorserver.cpp diff --git a/libnymea-remoteproxy/monitorserver.h b/libnymea-remoteproxy/server/monitorserver.h similarity index 100% rename from libnymea-remoteproxy/monitorserver.h rename to libnymea-remoteproxy/server/monitorserver.h diff --git a/libnymea-remoteproxy/tcpsocketserver.cpp b/libnymea-remoteproxy/server/tcpsocketserver.cpp similarity index 75% rename from libnymea-remoteproxy/tcpsocketserver.cpp rename to libnymea-remoteproxy/server/tcpsocketserver.cpp index 7fd358d..af7ebbf 100644 --- a/libnymea-remoteproxy/tcpsocketserver.cpp +++ b/libnymea-remoteproxy/server/tcpsocketserver.cpp @@ -1,23 +1,29 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * Copyright (C) 2019 Simon Stürz * - * * - * This file is part of nymea-remoteproxy. * - * * - * This program is free software: you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation, either version 3 of the License, or * - * (at your option) any later version. * - * * - * This program is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program. If not, see . * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "tcpsocketserver.h" #include "loggingcategories.h" @@ -106,7 +112,7 @@ bool TcpSocketServer::startServer() } connect(m_server, &SslServer::clientConnected, this, &TcpSocketServer::onClientConnected); - connect(m_server, SIGNAL(clientDisconnected(QSslSocket *)), SLOT(onClientDisconnected(QSslSocket *))); + connect(m_server, SIGNAL(clientDisconnected(QSslSocket*)), SLOT(onClientDisconnected(QSslSocket*))); connect(m_server, &SslServer::dataAvailable, this, &TcpSocketServer::onDataAvailable); qCDebug(dcTcpSocketServer()) << "Server started successfully."; return true; @@ -144,7 +150,7 @@ void SslServer::incomingConnection(qintptr socketDescriptor) qCDebug(dcTcpSocketServer()) << "Incomming connection" << sslSocket; connect(sslSocket, &QSslSocket::readyRead, this, &SslServer::onSocketReadyRead); connect(sslSocket, &QSslSocket::disconnected, this, &SslServer::onClientDisconnected); - connect(sslSocket, &QSslSocket::encrypted, [this, sslSocket](){ + connect(sslSocket, &QSslSocket::encrypted, this, [this, sslSocket](){ qCDebug(dcTcpSocketServer()) << "SSL encryption established for" << sslSocket; emit clientConnected(sslSocket); }); diff --git a/libnymea-remoteproxy/tcpsocketserver.h b/libnymea-remoteproxy/server/tcpsocketserver.h similarity index 100% rename from libnymea-remoteproxy/tcpsocketserver.h rename to libnymea-remoteproxy/server/tcpsocketserver.h diff --git a/libnymea-remoteproxy/transportinterface.cpp b/libnymea-remoteproxy/server/transportinterface.cpp similarity index 100% rename from libnymea-remoteproxy/transportinterface.cpp rename to libnymea-remoteproxy/server/transportinterface.cpp diff --git a/libnymea-remoteproxy/transportinterface.h b/libnymea-remoteproxy/server/transportinterface.h similarity index 100% rename from libnymea-remoteproxy/transportinterface.h rename to libnymea-remoteproxy/server/transportinterface.h diff --git a/libnymea-remoteproxy/websocketserver.cpp b/libnymea-remoteproxy/server/websocketserver.cpp similarity index 100% rename from libnymea-remoteproxy/websocketserver.cpp rename to libnymea-remoteproxy/server/websocketserver.cpp diff --git a/libnymea-remoteproxy/websocketserver.h b/libnymea-remoteproxy/server/websocketserver.h similarity index 100% rename from libnymea-remoteproxy/websocketserver.h rename to libnymea-remoteproxy/server/websocketserver.h diff --git a/libnymea-remoteproxy/tunnelproxy/tunnelproxymanager.cpp b/libnymea-remoteproxy/tunnelproxy/tunnelproxymanager.cpp new file mode 100644 index 0000000..5e692a7 --- /dev/null +++ b/libnymea-remoteproxy/tunnelproxy/tunnelproxymanager.cpp @@ -0,0 +1,141 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "tunnelproxymanager.h" +#include "loggingcategories.h" + +#include "jsonrpc/tunnelproxyhandler.h" + +namespace remoteproxy { + +TunnelProxyManager::TunnelProxyManager(QObject *parent) : + QObject(parent) +{ + m_jsonRpcServer = new JsonRpcServer(this); + m_jsonRpcServer->registerHandler(m_jsonRpcServer); + m_jsonRpcServer->registerHandler(new TunnelProxyHandler(this)); + +} + +TunnelProxyManager::~TunnelProxyManager() +{ + +} + +bool TunnelProxyManager::running() const +{ + return m_running; +} + +void TunnelProxyManager::setRunning(bool running) +{ + if (m_running == running) + return; + + qCDebug(dcTunnelProxyManager()) << "The proxy tunnel manager is" << (running ? "now up and running." : "not running any more."); + m_running = running; + emit runningChanged(m_running); +} + +void TunnelProxyManager::registerTransportInterface(TransportInterface *interface) +{ + qCDebug(dcTunnelProxyManager()) << "Register transport interface" << interface->serverName(); + + if (m_transportInterfaces.contains(interface)) { + qCWarning(dcTunnelProxyManager()) << "Transport interface already registerd."; + return; + } + + connect(interface, &TransportInterface::clientConnected, this, &TunnelProxyManager::onClientConnected); + connect(interface, &TransportInterface::clientDisconnected, this, &TunnelProxyManager::onClientDisconnected); + connect(interface, &TransportInterface::dataAvailable, this, &TunnelProxyManager::onClientDataAvailable); + + m_transportInterfaces.append(interface); +} + +TunnelProxyManager::Error TunnelProxyManager::registerServer(const QUuid &clientId, const QUuid &serverUuid, const QString &serverName) +{ + qCDebug(dcTunnelProxyManager()) << "Register new server" << m_proxyClients.value(clientId) << serverName << serverUuid.toString(); + + // TODO: check if uuid already exists + + // Check if requested already as client + + + TunnelProxyServer *proxyServer = new TunnelProxyServer(m_proxyClients.value(clientId), serverUuid, serverName); + m_proxyClientsTunnelServer.insert(clientId, proxyServer); + m_tunnelServers.insert(proxyServer->serverUuid(), proxyServer); + + return ErrorNoError; +} + +void TunnelProxyManager::startServer() +{ + qCDebug(dcTunnelProxyManager()) << "Starting tunnel proxy manager..."; + foreach (TransportInterface *interface, m_transportInterfaces) { + interface->startServer(); + } + setRunning(true); +} + +void TunnelProxyManager::stopServer() +{ + qCDebug(dcTunnelProxyManager()) << "Stopping tunnel proxy server..."; + foreach (TransportInterface *interface, m_transportInterfaces) { + interface->stopServer(); + } + setRunning(false); +} + +void TunnelProxyManager::tick() +{ + +} + +void TunnelProxyManager::onClientConnected(const QUuid &clientId, const QHostAddress &address) +{ + TransportInterface *interface = static_cast(sender()); + + qCDebug(dcTunnelProxyManager()) << "New client connected" << interface->serverName() << clientId.toString() << address.toString(); + + ProxyClient *proxyClient = new ProxyClient(interface, clientId, address, this); + m_proxyClients.insert(clientId, proxyClient); + m_jsonRpcServer->registerClient(proxyClient); +} + +void TunnelProxyManager::onClientDisconnected(const QUuid &clientId) +{ + Q_UNUSED(clientId) +} + +void TunnelProxyManager::onClientDataAvailable(const QUuid &clientId, const QByteArray &data) +{ + Q_UNUSED(clientId) + Q_UNUSED(data) +} + +} diff --git a/libnymea-remoteproxy/tunnelproxy/tunnelproxymanager.h b/libnymea-remoteproxy/tunnelproxy/tunnelproxymanager.h new file mode 100644 index 0000000..985bec1 --- /dev/null +++ b/libnymea-remoteproxy/tunnelproxy/tunnelproxymanager.h @@ -0,0 +1,93 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef TUNNELPROXYMANAGER_H +#define TUNNELPROXYMANAGER_H + +#include + +#include "server/jsonrpcserver.h" +#include "server/transportinterface.h" + +#include "tunnelproxyserver.h" + +namespace remoteproxy { + +class TunnelProxyManager : public QObject +{ + Q_OBJECT +public: + enum Error { + ErrorNoError, + ErrorServerNotFound + }; + Q_ENUM(Error) + + explicit TunnelProxyManager(QObject *parent = nullptr); + ~TunnelProxyManager(); + + bool running() const; + void setRunning(bool running); + + void registerTransportInterface(TransportInterface *interface); + + TunnelProxyManager::Error registerServer(const QUuid &clientId, const QUuid &serverUuid, const QString &serverName); + +public slots: + void startServer(); + void stopServer(); + + void tick(); + +signals: + void runningChanged(bool running); + +private slots: + void onClientConnected(const QUuid &clientId, const QHostAddress &address); + void onClientDisconnected(const QUuid &clientId); + void onClientDataAvailable(const QUuid &clientId, const QByteArray &data); + +// void onTunnelProxyServerRegistered(); +// void onProxyTunnelClientRegistered(); + +private: + JsonRpcServer *m_jsonRpcServer = nullptr; + QList m_transportInterfaces; + + bool m_running = false; + + QHash m_proxyClients; // clients + + // Server connections + QHash m_proxyClientsTunnelServer; // clientUuid, object + QHash m_tunnelServers; // server uuid, object + +}; + +} + +#endif // TUNNELPROXYMANAGER_H diff --git a/libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.cpp b/libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.cpp new file mode 100644 index 0000000..b6c8153 --- /dev/null +++ b/libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.cpp @@ -0,0 +1,56 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "tunnelproxyserver.h" + +namespace remoteproxy { + +TunnelProxyServer::TunnelProxyServer(ProxyClient *proxyClient, const QUuid &serverUuid, const QString &serverName, QObject *parent) : + QObject(parent), + m_proxyClient(proxyClient), + m_serverUuid(serverUuid), + m_serverName(serverName) +{ + +} + +ProxyClient *TunnelProxyServer::proxyClient() const +{ + return m_proxyClient; +} + +QUuid TunnelProxyServer::serverUuid() const +{ + return m_serverUuid; +} + +QString TunnelProxyServer::serverName() const +{ + return m_serverName; +} + +} diff --git a/libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.h b/libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.h new file mode 100644 index 0000000..5fd9d10 --- /dev/null +++ b/libnymea-remoteproxy/tunnelproxy/tunnelproxyserver.h @@ -0,0 +1,59 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef TUNNELPROXYSERVER_H +#define TUNNELPROXYSERVER_H + +#include + +#include "proxyclient.h" + +namespace remoteproxy { + +class TunnelProxyServer : public QObject +{ + Q_OBJECT +public: + explicit TunnelProxyServer(ProxyClient *proxyClient, const QUuid &serverUuid, const QString &serverName, QObject *parent = nullptr); + + ProxyClient *proxyClient() const; + + QUuid serverUuid() const; + QString serverName() const; + +signals: + +private: + ProxyClient *m_proxyClient = nullptr; + + QUuid m_serverUuid; + QString m_serverName; +}; + +} + +#endif // TUNNELPROXYSERVER_H diff --git a/monitor/main.cpp b/monitor/main.cpp index ec76991..87bf742 100644 --- a/monitor/main.cpp +++ b/monitor/main.cpp @@ -48,7 +48,7 @@ int main(int argc, char *argv[]) parser.setApplicationDescription(QString("\nThe nymea remote proxy monitor allowes to monitor the live server activity on the a local instance.\n\n" "Server version: %1\n" "API version: %2\n\n" - "Copyright %3 2018 Simon Stürz \n") + "Copyright %3 2021 nymea GmbH \n") .arg(SERVER_VERSION_STRING) .arg(API_VERSION_STRING) .arg(QChar(0xA9))); diff --git a/monitor/terminalwindow.cpp b/monitor/terminalwindow.cpp index 8842154..0942f21 100644 --- a/monitor/terminalwindow.cpp +++ b/monitor/terminalwindow.cpp @@ -101,7 +101,7 @@ const char *TerminalWindow::convertString(const QString &string) QString TerminalWindow::getDurationString(uint timestamp) { - uint duration = QDateTime::currentDateTime().toTime_t() - timestamp; + uint duration = QDateTime::currentDateTimeUtc().toTime_t() - timestamp; int seconds = static_cast(duration % 60); duration /= 60; int minutes = static_cast(duration % 60); diff --git a/server/main.cpp b/server/main.cpp index 1e95df4..dd6258b 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -65,7 +65,7 @@ static const char *const error = "\e[31m"; static void consoleLogHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { QString messageString; - QString timeString = QDateTime::currentDateTime().toString("yyyy.MM.dd hh:mm:ss.zzz"); + QString timeString = QDateTime::currentDateTimeUtc().toString("yyyy.MM.dd hh:mm:ss.zzz"); switch (type) { case QtInfoMsg: messageString = QString(" I %1 | %2: %3").arg(timeString).arg(context.category).arg(message); @@ -174,7 +174,7 @@ int main(int argc, char *argv[]) } // Verify webserver configuration - if (configuration->webSocketServerHost().isNull()) { + if (configuration->webSocketServerProxyHost().isNull()) { qCCritical(dcApplication()) << "Invalid web socket host address passed."; exit(-1); } diff --git a/tests/test-offline/nymea-remoteproxy-tests-offline.cpp b/tests/test-proxy/remoteproxytestsproxy.cpp similarity index 91% rename from tests/test-offline/nymea-remoteproxy-tests-offline.cpp rename to tests/test-proxy/remoteproxytestsproxy.cpp index 8d373ab..d9fddd0 100644 --- a/tests/test-offline/nymea-remoteproxy-tests-offline.cpp +++ b/tests/test-proxy/remoteproxytestsproxy.cpp @@ -25,7 +25,7 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -#include "nymea-remoteproxy-tests-offline.h" +#include "remoteproxytestsproxy.h" #include "engine.h" #include "loggingcategories.h" @@ -37,19 +37,19 @@ #include #include -RemoteProxyOfflineTests::RemoteProxyOfflineTests(QObject *parent) : +RemoteProxyTestsProxy::RemoteProxyTestsProxy(QObject *parent) : BaseTest(parent) { } -void RemoteProxyOfflineTests::startStopServer() +void RemoteProxyTestsProxy::startStopServer() { startServer(); stopServer(); } -void RemoteProxyOfflineTests::dummyAuthenticator() +void RemoteProxyTestsProxy::dummyAuthenticator() { cleanUpEngine(); @@ -69,7 +69,7 @@ void RemoteProxyOfflineTests::dummyAuthenticator() // Make sure the server is running QVERIFY(Engine::instance()->running()); - QVERIFY(Engine::instance()->webSocketServer()->running()); + QVERIFY(Engine::instance()->webSocketServerProxy()->running()); QVERIFY(Engine::instance()->proxyServer()->running()); // Create request @@ -78,7 +78,7 @@ void RemoteProxyOfflineTests::dummyAuthenticator() params.insert("name", "test"); params.insert("token", "foobar"); - QVariant response = invokeWebSocketApiCall("Authentication.Authenticate", params); + QVariant response = invokeWebSocketProxyApiCall("Authentication.Authenticate", params); qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); verifyAuthenticationError(response); @@ -86,7 +86,7 @@ void RemoteProxyOfflineTests::dummyAuthenticator() } -void RemoteProxyOfflineTests::monitorServer() +void RemoteProxyTestsProxy::monitorServer() { // Start the server startServer(); @@ -188,7 +188,7 @@ void RemoteProxyOfflineTests::monitorServer() stopServer(); } -void RemoteProxyOfflineTests::configuration_data() +void RemoteProxyTestsProxy::configuration_data() { QTest::addColumn("fileName"); QTest::addColumn("success"); @@ -201,7 +201,7 @@ void RemoteProxyOfflineTests::configuration_data() QTest::newRow("faulty chain") << ":/test-configuration-faulty-chain.conf" << false; } -void RemoteProxyOfflineTests::configuration() +void RemoteProxyTestsProxy::configuration() { QFETCH(QString, fileName); QFETCH(bool, success); @@ -210,7 +210,7 @@ void RemoteProxyOfflineTests::configuration() QCOMPARE(configuration.loadConfiguration(fileName), success); } -void RemoteProxyOfflineTests::serverPortBlocked() +void RemoteProxyTestsProxy::serverPortBlocked() { cleanUpEngine(); @@ -222,7 +222,7 @@ void RemoteProxyOfflineTests::serverPortBlocked() // Create a dummy server which blocks the port QWebSocketServer dummyServer("dummy-server", QWebSocketServer::NonSecureMode); - QVERIFY(dummyServer.listen(QHostAddress::LocalHost, m_configuration->webSocketServerPort())); + QVERIFY(dummyServer.listen(QHostAddress::LocalHost, m_configuration->webSocketServerProxyPort())); // Start proxy webserver QSignalSpy runningSpy(Engine::instance(), &Engine::runningChanged); @@ -235,7 +235,7 @@ void RemoteProxyOfflineTests::serverPortBlocked() QVERIFY(Engine::instance()->running()); // Make sure the websocket server is not running - QVERIFY(!Engine::instance()->webSocketServer()->running()); + QVERIFY(!Engine::instance()->webSocketServerProxy()->running()); QSignalSpy closedSpy(&dummyServer, &QWebSocketServer::closed); dummyServer.close(); @@ -272,7 +272,7 @@ void RemoteProxyOfflineTests::serverPortBlocked() QVERIFY(Engine::instance()->running()); // Make sure the TCP server is not running - QVERIFY(!Engine::instance()->tcpSocketServer()->running()); + QVERIFY(!Engine::instance()->tcpSocketServerProxy()->running()); tcpDummyServer->close(); delete tcpDummyServer; @@ -281,14 +281,14 @@ void RemoteProxyOfflineTests::serverPortBlocked() startServer(); // Make sure the TCP server is not running - QVERIFY(Engine::instance()->webSocketServer()->running()); - QVERIFY(Engine::instance()->tcpSocketServer()->running()); + QVERIFY(Engine::instance()->webSocketServerProxy()->running()); + QVERIFY(Engine::instance()->tcpSocketServerProxy()->running()); // Clean up stopServer(); } -void RemoteProxyOfflineTests::websocketBinaryData() +void RemoteProxyTestsProxy::websocketBinaryData() { // Start the server startServer(); @@ -296,7 +296,7 @@ void RemoteProxyOfflineTests::websocketBinaryData() QWebSocket *client = new QWebSocket("bad-client", QWebSocketProtocol::Version13); connect(client, &QWebSocket::sslErrors, this, &BaseTest::sslErrors); QSignalSpy spyConnection(client, SIGNAL(connected())); - client->open(Engine::instance()->webSocketServer()->serverUrl()); + client->open(Engine::instance()->webSocketServerProxy()->serverUrl()); spyConnection.wait(); // Send binary data and make sure the server disconnects this socket @@ -309,7 +309,7 @@ void RemoteProxyOfflineTests::websocketBinaryData() stopServer(); } -void RemoteProxyOfflineTests::websocketPing() +void RemoteProxyTestsProxy::websocketPing() { // Start the server startServer(); @@ -317,7 +317,7 @@ void RemoteProxyOfflineTests::websocketPing() QWebSocket *client = new QWebSocket("bad-client", QWebSocketProtocol::Version13); connect(client, &QWebSocket::sslErrors, this, &BaseTest::sslErrors); QSignalSpy spyConnection(client, SIGNAL(connected())); - client->open(Engine::instance()->webSocketServer()->serverUrl()); + client->open(Engine::instance()->webSocketServerProxy()->serverUrl()); spyConnection.wait(); QVERIFY(spyConnection.count() == 1); @@ -334,45 +334,45 @@ void RemoteProxyOfflineTests::websocketPing() stopServer(); } -//void RemoteProxyOfflineTests::apiBasicCallsTcp_data() -//{ -// QTest::addColumn("data"); -// QTest::addColumn("responseId"); -// QTest::addColumn("responseStatus"); +void RemoteProxyTestsProxy::apiBasicCallsTcp_data() +{ + QTest::addColumn("data"); + QTest::addColumn("responseId"); + QTest::addColumn("responseStatus"); -// QTest::newRow("valid call") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Hello\"}") << 42 << "success"; -// QTest::newRow("missing id") << QByteArray("{\"method\":\"RemoteProxy.Hello\"}") << -1 << "error"; -// QTest::newRow("missing method") << QByteArray("{\"id\":42}") << 42 << "error"; -// QTest::newRow("invalid json") << QByteArray("{\"id\":42, \"method\":\"RemoteProx") << -1 << "error"; -// QTest::newRow("invalid function") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Explode\"}") << 42 << "error"; -// QTest::newRow("invalid namespace") << QByteArray("{\"id\":42, \"method\":\"ProxyRemote.Hello\"}") << 42 << "error"; -// QTest::newRow("missing dot") << QByteArray("{\"id\":42, \"method\":\"RemoteProxyHello\"}") << 42 << "error"; -// QTest::newRow("invalid params") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Hello\", \"params\":{\"törööö\":\"chooo-chooo\"}}") << 42 << "error"; -// QTest::newRow("invalid authentication params") << QByteArray("{\"id\":42, \"method\":\"Authentication.Authenticate\", \"params\":{\"your\":\"mamma\"}}") << 42 << "error"; -//} + QTest::newRow("valid call") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Hello\"}") << 42 << "success"; + QTest::newRow("missing id") << QByteArray("{\"method\":\"RemoteProxy.Hello\"}") << -1 << "error"; + QTest::newRow("missing method") << QByteArray("{\"id\":42}") << 42 << "error"; + //QTest::newRow("invalid json") << QByteArray("{\"id\":42, \"method\":\"RemoteProx") << -1 << "error"; + QTest::newRow("invalid function") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Explode\"}") << 42 << "error"; + QTest::newRow("invalid namespace") << QByteArray("{\"id\":42, \"method\":\"ProxyRemote.Hello\"}") << 42 << "error"; + QTest::newRow("missing dot") << QByteArray("{\"id\":42, \"method\":\"RemoteProxyHello\"}") << 42 << "error"; + QTest::newRow("invalid params") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Hello\", \"params\":{\"törööö\":\"chooo-chooo\"}}") << 42 << "error"; + QTest::newRow("invalid authentication params") << QByteArray("{\"id\":42, \"method\":\"Authentication.Authenticate\", \"params\":{\"your\":\"mamma\"}}") << 42 << "error"; +} -//void RemoteProxyOfflineTests::apiBasicCallsTcp() -//{ -// QFETCH(QByteArray, data); -// QFETCH(int, responseId); -// QFETCH(QString, responseStatus); +void RemoteProxyTestsProxy::apiBasicCallsTcp() +{ + QFETCH(QByteArray, data); + QFETCH(int, responseId); + QFETCH(QString, responseStatus); -// // Start the server -// startServer(); + // Start the server + startServer(); -// QVariant response = injectTcpSocketData(data); -// QVERIFY(!response.isNull()); + QVariant response = injectTcpSocketProxyData(data); + QVERIFY(!response.isNull()); -// qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); -// QCOMPARE(response.toMap().value("id").toInt(), responseId); -// QCOMPARE(response.toMap().value("status").toString(), responseStatus); + QCOMPARE(response.toMap().value("id").toInt(), responseId); + QCOMPARE(response.toMap().value("status").toString(), responseStatus); -// // Clean up -// stopServer(); -//} + // Clean up + stopServer(); +} -void RemoteProxyOfflineTests::getIntrospect() +void RemoteProxyTestsProxy::getIntrospect() { // Start the server startServer(); @@ -380,7 +380,7 @@ void RemoteProxyOfflineTests::getIntrospect() QVariantMap response; // WebSocket - response = invokeWebSocketApiCall("RemoteProxy.Introspect").toMap(); + response = invokeWebSocketProxyApiCall("RemoteProxy.Introspect").toMap(); //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); QVERIFY(!response.isEmpty()); QVERIFY(response.value("status").toString() == "success"); @@ -390,7 +390,7 @@ void RemoteProxyOfflineTests::getIntrospect() // Tcp response.clear(); - response = invokeTcpSocketApiCall("RemoteProxy.Introspect").toMap(); + response = invokeTcpSocketProxyApiCall("RemoteProxy.Introspect").toMap(); //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); QVERIFY(!response.isEmpty()); @@ -403,14 +403,14 @@ void RemoteProxyOfflineTests::getIntrospect() stopServer(); } -void RemoteProxyOfflineTests::getHello() +void RemoteProxyTestsProxy::getHello() { // Start the server startServer(); QVariantMap response; // WebSocket - response = invokeWebSocketApiCall("RemoteProxy.Hello").toMap(); + response = invokeWebSocketProxyApiCall("RemoteProxy.Hello").toMap(); //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); // Verify data @@ -422,7 +422,7 @@ void RemoteProxyOfflineTests::getHello() // TCP response.clear(); - response = invokeTcpSocketApiCall("RemoteProxy.Hello").toMap(); + response = invokeTcpSocketProxyApiCall("RemoteProxy.Hello").toMap(); //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); // Verify data @@ -437,7 +437,7 @@ void RemoteProxyOfflineTests::getHello() stopServer(); } -void RemoteProxyOfflineTests::apiBasicCalls_data() +void RemoteProxyTestsProxy::apiBasicCalls_data() { QTest::addColumn("data"); QTest::addColumn("responseId"); @@ -454,7 +454,7 @@ void RemoteProxyOfflineTests::apiBasicCalls_data() QTest::newRow("invalid authentication params") << QByteArray("{\"id\":42, \"method\":\"Authentication.Authenticate\", \"params\":{\"your\":\"mamma\"}}") << 42 << "error"; } -void RemoteProxyOfflineTests::apiBasicCalls() +void RemoteProxyTestsProxy::apiBasicCalls() { QFETCH(QByteArray, data); QFETCH(int, responseId); @@ -466,7 +466,7 @@ void RemoteProxyOfflineTests::apiBasicCalls() QVariantMap response; // Websocket - response = injectWebSocketData(data).toMap(); + response = injectWebSocketProxyData(data).toMap(); //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); QVERIFY(!response.isEmpty()); QCOMPARE(response.value("id").toInt(), responseId); @@ -474,7 +474,7 @@ void RemoteProxyOfflineTests::apiBasicCalls() // TCP response.clear(); - response = injectTcpSocketData(data).toMap(); + response = injectTcpSocketProxyData(data).toMap(); //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); QVERIFY(!response.isEmpty()); QCOMPARE(response.value("id").toInt(), responseId); @@ -484,7 +484,7 @@ void RemoteProxyOfflineTests::apiBasicCalls() stopServer(); } -void RemoteProxyOfflineTests::authenticate_data() +void RemoteProxyTestsProxy::authenticate_data() { QTest::addColumn("uuid"); QTest::addColumn("name"); @@ -518,7 +518,7 @@ void RemoteProxyOfflineTests::authenticate_data() } -void RemoteProxyOfflineTests::authenticate() +void RemoteProxyTestsProxy::authenticate() { QFETCH(QString, uuid); QFETCH(QString, name); @@ -543,19 +543,19 @@ void RemoteProxyOfflineTests::authenticate() // WebSocket QVariantMap response; - response = invokeWebSocketApiCall("Authentication.Authenticate", params).toMap(); + response = invokeWebSocketProxyApiCall("Authentication.Authenticate", params).toMap(); //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); verifyAuthenticationError(response, expectedError); // TCP - response = invokeTcpSocketApiCall("Authentication.Authenticate", params).toMap(); + response = invokeTcpSocketProxyApiCall("Authentication.Authenticate", params).toMap(); verifyAuthenticationError(response, expectedError); // Clean up stopServer(); } -void RemoteProxyOfflineTests::authenticateNonce() +void RemoteProxyTestsProxy::authenticateNonce() { // Start the server startServer(); @@ -651,7 +651,7 @@ void RemoteProxyOfflineTests::authenticateNonce() stopServer(); } -void RemoteProxyOfflineTests::authenticateSendData() +void RemoteProxyTestsProxy::authenticateSendData() { // Start the server startServer(); @@ -675,7 +675,7 @@ void RemoteProxyOfflineTests::authenticateSendData() QWebSocket *socket = new QWebSocket("proxy-testclient", QWebSocketProtocol::Version13); connect(socket, &QWebSocket::sslErrors, this, &BaseTest::sslErrors); QSignalSpy spyConnection(socket, &QWebSocket::connected); - socket->open(Engine::instance()->webSocketServer()->serverUrl()); + socket->open(Engine::instance()->webSocketServerProxy()->serverUrl()); spyConnection.wait(); QVERIFY(spyConnection.count() == 1); @@ -697,7 +697,7 @@ void RemoteProxyOfflineTests::authenticateSendData() stopServer(); } -void RemoteProxyOfflineTests::clientConnectionWebSocket() +void RemoteProxyTestsProxy::clientConnectionWebSocket() { // Start the server startServer(); @@ -747,7 +747,7 @@ void RemoteProxyOfflineTests::clientConnectionWebSocket() stopServer(); } -void RemoteProxyOfflineTests::clientConnectionTcpSocket() +void RemoteProxyTestsProxy::clientConnectionTcpSocket() { // Start the server startServer(); @@ -797,7 +797,7 @@ void RemoteProxyOfflineTests::clientConnectionTcpSocket() stopServer(); } -void RemoteProxyOfflineTests::remoteConnection() +void RemoteProxyTestsProxy::remoteConnection() { // Start the server startServer(); @@ -890,7 +890,7 @@ void RemoteProxyOfflineTests::remoteConnection() stopServer(); } -void RemoteProxyOfflineTests::multipleRemoteConnection() +void RemoteProxyTestsProxy::multipleRemoteConnection() { // Start the server startServer(); @@ -920,7 +920,7 @@ void RemoteProxyOfflineTests::multipleRemoteConnection() stopServer(); } -void RemoteProxyOfflineTests::trippleConnection() +void RemoteProxyTestsProxy::trippleConnection() { // Start the server startServer(); @@ -1005,7 +1005,7 @@ void RemoteProxyOfflineTests::trippleConnection() stopServer(); } -void RemoteProxyOfflineTests::duplicateUuid() +void RemoteProxyTestsProxy::duplicateUuid() { // Start the server startServer(); @@ -1067,7 +1067,7 @@ void RemoteProxyOfflineTests::duplicateUuid() stopServer(); } -void RemoteProxyOfflineTests::sslConfigurations() +void RemoteProxyTestsProxy::sslConfigurations() { // Start the server startServer(); @@ -1098,7 +1098,7 @@ void RemoteProxyOfflineTests::sslConfigurations() stopServer(); } -void RemoteProxyOfflineTests::jsonRpcTimeout() +void RemoteProxyTestsProxy::jsonRpcTimeout() { // Start the server startServer(); @@ -1120,7 +1120,7 @@ void RemoteProxyOfflineTests::jsonRpcTimeout() params.insert("name", "name"); params.insert("token", "token"); - QVariant response = invokeWebSocketApiCall("Authentication.Authenticate", params); + QVariant response = invokeWebSocketProxyApiCall("Authentication.Authenticate", params); qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); QVERIFY(response.toMap().value("status").toString() == "error"); @@ -1129,7 +1129,7 @@ void RemoteProxyOfflineTests::jsonRpcTimeout() stopServer(); } -void RemoteProxyOfflineTests::inactiveTimeout() +void RemoteProxyTestsProxy::inactiveTimeout() { // Start the server startServer(); @@ -1152,7 +1152,7 @@ void RemoteProxyOfflineTests::inactiveTimeout() stopServer(); } -void RemoteProxyOfflineTests::authenticationReplyTimeout() +void RemoteProxyTestsProxy::authenticationReplyTimeout() { // Start the server startServer(); @@ -1171,7 +1171,7 @@ void RemoteProxyOfflineTests::authenticationReplyTimeout() params.insert("name", "Sleepy test client"); params.insert("token", "sleepy token zzzZZZ"); - QVariant response = invokeWebSocketApiCall("Authentication.Authenticate", params); + QVariant response = invokeWebSocketProxyApiCall("Authentication.Authenticate", params); qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); verifyAuthenticationError(response, Authenticator::AuthenticationErrorTimeout); @@ -1179,7 +1179,7 @@ void RemoteProxyOfflineTests::authenticationReplyTimeout() stopServer(); } -void RemoteProxyOfflineTests::authenticationReplyConnection() +void RemoteProxyTestsProxy::authenticationReplyConnection() { // Start the server startServer(); @@ -1212,7 +1212,7 @@ void RemoteProxyOfflineTests::authenticationReplyConnection() stopServer(); } -void RemoteProxyOfflineTests::tcpRemoteConnection() +void RemoteProxyTestsProxy::tcpRemoteConnection() { // Start the server startServer(); @@ -1306,7 +1306,7 @@ void RemoteProxyOfflineTests::tcpRemoteConnection() stopServer(); } -void RemoteProxyOfflineTests::tcpWebsocketRemoteConnection() +void RemoteProxyTestsProxy::tcpWebsocketRemoteConnection() { // Start the server startServer(); @@ -1400,4 +1400,4 @@ void RemoteProxyOfflineTests::tcpWebsocketRemoteConnection() stopServer(); } -QTEST_MAIN(RemoteProxyOfflineTests) +QTEST_MAIN(RemoteProxyTestsProxy) diff --git a/tests/test-offline/nymea-remoteproxy-tests-offline.h b/tests/test-proxy/remoteproxytestsproxy.h similarity index 89% rename from tests/test-offline/nymea-remoteproxy-tests-offline.h rename to tests/test-proxy/remoteproxytestsproxy.h index dc0a233..43c120f 100644 --- a/tests/test-offline/nymea-remoteproxy-tests-offline.h +++ b/tests/test-proxy/remoteproxytestsproxy.h @@ -25,20 +25,20 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -#ifndef NYMEA_REMOTEPROXY_TESTS_OFFLINE_H -#define NYMEA_REMOTEPROXY_TESTS_OFFLINE_H +#ifndef REMOTEPROXYTESTSPROXY_H +#define REMOTEPROXYTESTSPROXY_H #include "basetest.h" using namespace remoteproxy; using namespace remoteproxyclient; -class RemoteProxyOfflineTests : public BaseTest +class RemoteProxyTestsProxy : public BaseTest { Q_OBJECT public: - explicit RemoteProxyOfflineTests(QObject *parent = nullptr); - ~RemoteProxyOfflineTests() = default; + explicit RemoteProxyTestsProxy(QObject *parent = nullptr); + ~RemoteProxyTestsProxy() = default; private slots: // Basic stuff @@ -61,6 +61,9 @@ private slots: void apiBasicCalls_data(); void apiBasicCalls(); + void apiBasicCallsTcp_data(); + void apiBasicCallsTcp(); + void authenticate_data(); void authenticate(); @@ -87,4 +90,4 @@ private slots: }; -#endif // NYMEA_REMOTEPROXY_TESTS_OFFLINE_H +#endif // REMOTEPROXYTESTSPROXY_H diff --git a/tests/test-offline/test-offline.pro b/tests/test-proxy/test-proxy.pro similarity index 55% rename from tests/test-offline/test-offline.pro rename to tests/test-proxy/test-proxy.pro index c6db854..dd5a788 100644 --- a/tests/test-offline/test-offline.pro +++ b/tests/test-proxy/test-proxy.pro @@ -4,11 +4,11 @@ include(../testbase/testbase.pri) CONFIG += testcase QT += testlib -TARGET = nymea-remoteproxy-tests-offline +TARGET = nymea-remoteproxy-proxy-tests -HEADERS += nymea-remoteproxy-tests-offline.h +HEADERS += remoteproxytestsproxy.h -SOURCES += nymea-remoteproxy-tests-offline.cpp +SOURCES += remoteproxytestsproxy.cpp target.path = $$[QT_INSTALL_PREFIX]/bin INSTALLS += target diff --git a/tests/test-tunnelproxy/remoteproxyteststunnelproxy.cpp b/tests/test-tunnelproxy/remoteproxyteststunnelproxy.cpp new file mode 100644 index 0000000..990daef --- /dev/null +++ b/tests/test-tunnelproxy/remoteproxyteststunnelproxy.cpp @@ -0,0 +1,202 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "remoteproxyteststunnelproxy.h" + +#include "engine.h" +#include "loggingcategories.h" +#include "remoteproxyconnection.h" + +#include +#include +#include +#include +#include + +RemoteProxyTestsTunnelProxy::RemoteProxyTestsTunnelProxy(QObject *parent) : + BaseTest(parent) +{ + +} + +void RemoteProxyTestsTunnelProxy::startStopServer() +{ + startServer(); + stopServer(); +} + +void RemoteProxyTestsTunnelProxy::apiBasicCallsTcp_data() +{ + QTest::addColumn("data"); + QTest::addColumn("responseId"); + QTest::addColumn("responseStatus"); + + QTest::newRow("valid call") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Hello\"}") << 42 << "success"; + QTest::newRow("missing id") << QByteArray("{\"method\":\"RemoteProxy.Hello\"}") << -1 << "error"; + QTest::newRow("missing method") << QByteArray("{\"id\":42}") << 42 << "error"; + //QTest::newRow("invalid json") << QByteArray("{\"id\":42, \"method\":\"RemoteProx") << -1 << "error"; + QTest::newRow("invalid function") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Explode\"}") << 42 << "error"; + QTest::newRow("invalid namespace") << QByteArray("{\"id\":42, \"method\":\"ProxyRemote.Hello\"}") << 42 << "error"; + QTest::newRow("missing dot") << QByteArray("{\"id\":42, \"method\":\"RemoteProxyHello\"}") << 42 << "error"; + QTest::newRow("invalid params") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Hello\", \"params\":{\"törööö\":\"chooo-chooo\"}}") << 42 << "error"; + QTest::newRow("invalid authentication params") << QByteArray("{\"id\":42, \"method\":\"Authentication.Authenticate\", \"params\":{\"your\":\"mamma\"}}") << 42 << "error"; +} + +void RemoteProxyTestsTunnelProxy::apiBasicCallsTcp() +{ + QFETCH(QByteArray, data); + QFETCH(int, responseId); + QFETCH(QString, responseStatus); + + // Start the server + startServer(); + + QVariant response = injectTcpSocketProxyData(data); + QVERIFY(!response.isNull()); + + qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + + QCOMPARE(response.toMap().value("id").toInt(), responseId); + QCOMPARE(response.toMap().value("status").toString(), responseStatus); + + // Clean up + stopServer(); +} + +void RemoteProxyTestsTunnelProxy::getIntrospect() +{ + // Start the server + startServer(); + + QVariantMap response; + + // WebSocket + response = invokeWebSocketTunnelProxyApiCall("RemoteProxy.Introspect").toMap(); + //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + QVERIFY(!response.isEmpty()); + QVERIFY(response.value("status").toString() == "success"); + QVERIFY(response.value("params").toMap().contains("methods")); + QVERIFY(response.value("params").toMap().contains("notifications")); + QVERIFY(response.value("params").toMap().contains("types")); + + // Tcp + response.clear(); + response = invokeTcpSocketTunnelProxyApiCall("RemoteProxy.Introspect").toMap(); + //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + + QVERIFY(!response.isEmpty()); + QVERIFY(response.value("status").toString() == "success"); + QVERIFY(response.value("params").toMap().contains("methods")); + QVERIFY(response.value("params").toMap().contains("notifications")); + QVERIFY(response.value("params").toMap().contains("types")); + + // Clean up + stopServer(); +} + +void RemoteProxyTestsTunnelProxy::getHello() +{ + // Start the server + startServer(); + QVariantMap response; + + // WebSocket + response = invokeWebSocketTunnelProxyApiCall("RemoteProxy.Hello").toMap(); + qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + + // Verify data + QVERIFY(!response.isEmpty()); + QCOMPARE(response.value("params").toMap().value("name").toString(), Engine::instance()->configuration()->serverName()); + QCOMPARE(response.value("params").toMap().value("server").toString(), QString(SERVER_NAME_STRING)); + QCOMPARE(response.value("params").toMap().value("version").toString(), QString(SERVER_VERSION_STRING)); + QCOMPARE(response.value("params").toMap().value("apiVersion").toString(), QString(API_VERSION_STRING)); + + // TCP + response.clear(); + response = invokeTcpSocketTunnelProxyApiCall("RemoteProxy.Hello").toMap(); + //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + + // Verify data + QVERIFY(!response.isEmpty()); + QCOMPARE(response.value("params").toMap().value("name").toString(), Engine::instance()->configuration()->serverName()); + QCOMPARE(response.value("params").toMap().value("server").toString(), QString(SERVER_NAME_STRING)); + QCOMPARE(response.value("params").toMap().value("version").toString(), QString(SERVER_VERSION_STRING)); + QCOMPARE(response.value("params").toMap().value("apiVersion").toString(), QString(API_VERSION_STRING)); + + + // Clean up + stopServer(); +} + +void RemoteProxyTestsTunnelProxy::apiBasicCalls_data() +{ + QTest::addColumn("data"); + QTest::addColumn("responseId"); + QTest::addColumn("responseStatus"); + + QTest::newRow("valid call") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Hello\"}") << 42 << "success"; + QTest::newRow("missing id") << QByteArray("{\"method\":\"RemoteProxy.Hello\"}") << -1 << "error"; + QTest::newRow("missing method") << QByteArray("{\"id\":42}") << 42 << "error"; + QTest::newRow("invalid json") << QByteArray("{\"id\":42, \"method\":\"RemoteProx}") << -1 << "error"; + QTest::newRow("invalid function") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Explode\"}") << 42 << "error"; + QTest::newRow("invalid namespace") << QByteArray("{\"id\":42, \"method\":\"ProxyRemote.Hello\"}") << 42 << "error"; + QTest::newRow("missing dot") << QByteArray("{\"id\":42, \"method\":\"RemoteProxyHello\"}") << 42 << "error"; + QTest::newRow("invalid params") << QByteArray("{\"id\":42, \"method\":\"RemoteProxy.Hello\", \"params\":{\"törööö\":\"chooo-chooo\"}}") << 42 << "error"; + QTest::newRow("invalid authentication params") << QByteArray("{\"id\":42, \"method\":\"Authentication.Authenticate\", \"params\":{\"your\":\"mamma\"}}") << 42 << "error"; +} + +void RemoteProxyTestsTunnelProxy::apiBasicCalls() +{ + QFETCH(QByteArray, data); + QFETCH(int, responseId); + QFETCH(QString, responseStatus); + + // Start the server + startServer(); + + QVariantMap response; + + // Websocket + response = injectWebSocketTunnelProxyData(data).toMap(); + //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + QVERIFY(!response.isEmpty()); + QCOMPARE(response.value("id").toInt(), responseId); + QCOMPARE(response.value("status").toString(), responseStatus); + + // TCP + response.clear(); + response = injectTcpSocketTunnelProxyData(data).toMap(); + //qDebug() << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); + QVERIFY(!response.isEmpty()); + QCOMPARE(response.value("id").toInt(), responseId); + QCOMPARE(response.value("status").toString(), responseStatus); + + // Clean up + stopServer(); +} + +QTEST_MAIN(RemoteProxyTestsTunnelProxy) diff --git a/tests/test-tunnelproxy/remoteproxyteststunnelproxy.h b/tests/test-tunnelproxy/remoteproxyteststunnelproxy.h new file mode 100644 index 0000000..ceea833 --- /dev/null +++ b/tests/test-tunnelproxy/remoteproxyteststunnelproxy.h @@ -0,0 +1,59 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See the GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef REMOTEPROXYTESTSTUNNELPROXY_H +#define REMOTEPROXYTESTSTUNNELPROXY_H + +#include "basetest.h" + +using namespace remoteproxy; +using namespace remoteproxyclient; + +class RemoteProxyTestsTunnelProxy : public BaseTest +{ + Q_OBJECT +public: + explicit RemoteProxyTestsTunnelProxy(QObject *parent = nullptr); + ~RemoteProxyTestsTunnelProxy() = default; + +private slots: + // Basic stuff + void startStopServer(); + + // WebSocket connection API + void getIntrospect(); + void getHello(); + + void apiBasicCalls_data(); + void apiBasicCalls(); + + void apiBasicCallsTcp_data(); + void apiBasicCallsTcp(); + +}; + +#endif // REMOTEPROXYTESTSTUNNELPROXY_H diff --git a/tests/test-tunnelproxy/test-tunnelproxy.pro b/tests/test-tunnelproxy/test-tunnelproxy.pro new file mode 100644 index 0000000..aa34de2 --- /dev/null +++ b/tests/test-tunnelproxy/test-tunnelproxy.pro @@ -0,0 +1,14 @@ +include(../../nymea-remoteproxy.pri) +include(../testbase/testbase.pri) + +CONFIG += testcase +QT += testlib + +TARGET = nymea-remoteproxy-tunnelproxy-tests + +HEADERS += remoteproxyteststunnelproxy.h + +SOURCES += remoteproxyteststunnelproxy.cpp + +target.path = $$[QT_INSTALL_PREFIX]/bin +INSTALLS += target diff --git a/tests/testbase/basetest.cpp b/tests/testbase/basetest.cpp index 42ecbcc..5434282 100644 --- a/tests/testbase/basetest.cpp +++ b/tests/testbase/basetest.cpp @@ -115,8 +115,10 @@ void BaseTest::startServer() QVERIFY(Engine::instance()->running()); QVERIFY(Engine::instance()->developerMode()); - QVERIFY(Engine::instance()->webSocketServer()->running()); - QVERIFY(Engine::instance()->tcpSocketServer()->running()); + QVERIFY(Engine::instance()->webSocketServerProxy()->running()); + QVERIFY(Engine::instance()->tcpSocketServerProxy()->running()); + QVERIFY(Engine::instance()->webSocketServerTunnelProxy()->running()); + QVERIFY(Engine::instance()->tcpSocketServerTunnelProxy()->running()); QVERIFY(Engine::instance()->monitorServer()->running()); } @@ -131,7 +133,7 @@ void BaseTest::stopServer() cleanUpEngine(); } -QVariant BaseTest::invokeWebSocketApiCall(const QString &method, const QVariantMap params, bool remainsConnected) +QVariant BaseTest::invokeWebSocketProxyApiCall(const QString &method, const QVariantMap params, bool remainsConnected) { Q_UNUSED(remainsConnected) @@ -144,7 +146,7 @@ QVariant BaseTest::invokeWebSocketApiCall(const QString &method, const QVariantM QWebSocket *socket = new QWebSocket("proxy-testclient", QWebSocketProtocol::Version13); connect(socket, &QWebSocket::sslErrors, this, &BaseTest::sslErrors); QSignalSpy spyConnection(socket, SIGNAL(connected())); - socket->open(Engine::instance()->webSocketServer()->serverUrl()); + socket->open(Engine::instance()->webSocketServerProxy()->serverUrl()); spyConnection.wait(); if (spyConnection.count() == 0) { return QVariant(); @@ -187,13 +189,13 @@ QVariant BaseTest::invokeWebSocketApiCall(const QString &method, const QVariantM return QVariant(); } -QVariant BaseTest::injectWebSocketData(const QByteArray &data) +QVariant BaseTest::injectWebSocketProxyData(const QByteArray &data) { QWebSocket *socket = new QWebSocket("proxy-testclient", QWebSocketProtocol::Version13); connect(socket, &QWebSocket::sslErrors, this, &BaseTest::sslErrors); QSignalSpy spyConnection(socket, SIGNAL(connected())); - socket->open(Engine::instance()->webSocketServer()->serverUrl()); + socket->open(Engine::instance()->webSocketServerProxy()->serverUrl()); spyConnection.wait(); if (spyConnection.count() == 0) { return QVariant(); @@ -226,7 +228,7 @@ QVariant BaseTest::injectWebSocketData(const QByteArray &data) return QVariant(); } -QVariant BaseTest::invokeTcpSocketApiCall(const QString &method, const QVariantMap params, bool remainsConnected) +QVariant BaseTest::invokeTcpSocketProxyApiCall(const QString &method, const QVariantMap params, bool remainsConnected) { Q_UNUSED(remainsConnected) @@ -241,8 +243,8 @@ QVariant BaseTest::invokeTcpSocketApiCall(const QString &method, const QVariantM QObject::connect(socket, static_cast(&QSslSocket::sslErrors), this, &BaseTest::sslSocketSslErrors); QSignalSpy spyConnection(socket, &QSslSocket::connected); - socket->connectToHostEncrypted(Engine::instance()->tcpSocketServer()->serverUrl().host(), - static_cast(Engine::instance()->tcpSocketServer()->serverUrl().port())); + socket->connectToHostEncrypted(Engine::instance()->tcpSocketServerProxy()->serverUrl().host(), + static_cast(Engine::instance()->tcpSocketServerProxy()->serverUrl().port())); spyConnection.wait(); if (spyConnection.count() == 0) { return QVariant(); @@ -298,8 +300,8 @@ bool BaseTest::createRemoteConnection(const QString &token, const QString &nonce QString nameConnectionTwo = "Test client two"; QUuid uuidConnectionTwo = QUuid::createUuid(); - QByteArray dataOne = "Hello from client one :-)"; - QByteArray dataTwo = "Hello from client two :-)"; +// QByteArray dataOne = "Hello from client one :-)"; +// QByteArray dataTwo = "Hello from client two :-)"; // Create two connection RemoteProxyConnection *connectionOne = new RemoteProxyConnection(uuidConnectionOne, nameConnectionOne, parent); @@ -400,15 +402,216 @@ bool BaseTest::createRemoteConnection(const QString &token, const QString &nonce } -QVariant BaseTest::injectTcpSocketData(const QByteArray &data) +QVariant BaseTest::injectTcpSocketProxyData(const QByteArray &data) { QSslSocket *socket = new QSslSocket(this); typedef void (QSslSocket:: *sslErrorsSignal)(const QList &); QObject::connect(socket, static_cast(&QSslSocket::sslErrors), this, &BaseTest::sslSocketSslErrors); QSignalSpy spyConnection(socket, &QSslSocket::connected); - socket->connectToHostEncrypted(Engine::instance()->tcpSocketServer()->serverUrl().host(), - static_cast(Engine::instance()->tcpSocketServer()->serverUrl().port())); + socket->connectToHostEncrypted(Engine::instance()->tcpSocketServerProxy()->serverUrl().host(), + static_cast(Engine::instance()->tcpSocketServerProxy()->serverUrl().port())); + spyConnection.wait(); + if (spyConnection.count() == 0) { + return QVariant(); + } + + QSignalSpy dataSpy(socket, &QSslSocket::readyRead); + socket->write(data + '\n'); + dataSpy.wait(); + // FIXME: check why it waits the full time here + dataSpy.wait(500); + if (dataSpy.count() != 1) { + qWarning() << "No data received"; + return QVariant(); + } + + QByteArray socketData = socket->readAll(); + socket->close(); + socket->deleteLater(); + + // Make sure the response ends with '}\n' + if (!socketData.endsWith("}\n")) { + qWarning() << "JSON data does not end with \"}\n\""; + return QVariant(); + } + + // Make sure the response it a valid JSON string + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(socketData, &error); + if (error.error != QJsonParseError::NoError) { + qWarning() << "JSON parser error" << error.errorString(); + return QVariant(); + } + + m_commandCounter++; + return jsonDoc.toVariant(); +} + +QVariant BaseTest::invokeWebSocketTunnelProxyApiCall(const QString &method, const QVariantMap params, bool remainsConnected) +{ + Q_UNUSED(remainsConnected) + + QVariantMap request; + request.insert("id", m_commandCounter); + request.insert("method", method); + request.insert("params", params); + QJsonDocument jsonDoc = QJsonDocument::fromVariant(request); + + QWebSocket *socket = new QWebSocket("tunnelproxy-testclient", QWebSocketProtocol::Version13); + connect(socket, &QWebSocket::sslErrors, this, &BaseTest::sslErrors); + QSignalSpy spyConnection(socket, SIGNAL(connected())); + socket->open(Engine::instance()->webSocketServerTunnelProxy()->serverUrl()); + spyConnection.wait(); + if (spyConnection.count() == 0) { + return QVariant(); + } + + QSignalSpy dataSpy(socket, SIGNAL(textMessageReceived(QString))); + socket->sendTextMessage(QString(jsonDoc.toJson(QJsonDocument::Compact))); + dataSpy.wait(); + + socket->close(); + socket->deleteLater(); + + 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 QVariant(); + } + + // Make sure the response it a valid JSON string + QJsonParseError error; + jsonDoc = QJsonDocument::fromJson(dataSpy.at(i).last().toByteArray(), &error); + if (error.error != QJsonParseError::NoError) { + qWarning() << "JSON parser error" << error.errorString(); + return QVariant(); + } + QVariantMap response = jsonDoc.toVariant().toMap(); + + // skip notifications + if (response.contains("notification")) + continue; + + if (response.value("id").toInt() == m_commandCounter) { + m_commandCounter++; + return jsonDoc.toVariant(); + } + } + + m_commandCounter++; + return QVariant(); +} + +QVariant BaseTest::injectWebSocketTunnelProxyData(const QByteArray &data) +{ + QWebSocket *socket = new QWebSocket("tunnelproxy-testclient", QWebSocketProtocol::Version13); + connect(socket, &QWebSocket::sslErrors, this, &BaseTest::sslErrors); + + QSignalSpy spyConnection(socket, SIGNAL(connected())); + socket->open(Engine::instance()->webSocketServerTunnelProxy()->serverUrl()); + spyConnection.wait(); + if (spyConnection.count() == 0) { + return QVariant(); + } + + QSignalSpy spy(socket, SIGNAL(textMessageReceived(QString))); + socket->sendTextMessage(QString(data)); + spy.wait(); + + socket->close(); + socket->deleteLater(); + + for (int i = 0; i < spy.count(); i++) { + // Make sure the response ends with '}\n' + if (!spy.at(i).last().toByteArray().endsWith("}\n")) { + qWarning() << "JSON data does not end with \"}\n\""; + return QVariant(); + } + + // Make sure the response it a valid JSON string + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.at(i).last().toByteArray(), &error); + if (error.error != QJsonParseError::NoError) { + qWarning() << "JSON parser error" << error.errorString(); + return QVariant(); + } + return jsonDoc.toVariant(); + } + m_commandCounter++; + return QVariant(); +} + +QVariant BaseTest::invokeTcpSocketTunnelProxyApiCall(const QString &method, const QVariantMap params, bool remainsConnected) +{ + Q_UNUSED(remainsConnected) + + QVariantMap request; + request.insert("id", m_commandCounter); + request.insert("method", method); + request.insert("params", params); + QJsonDocument jsonDoc = QJsonDocument::fromVariant(request); + + QSslSocket *socket = new QSslSocket(this); + typedef void (QSslSocket:: *sslErrorsSignal)(const QList &); + QObject::connect(socket, static_cast(&QSslSocket::sslErrors), this, &BaseTest::sslSocketSslErrors); + + QSignalSpy spyConnection(socket, &QSslSocket::connected); + socket->connectToHostEncrypted(Engine::instance()->tcpSocketServerTunnelProxy()->serverUrl().host(), + static_cast(Engine::instance()->tcpSocketServerTunnelProxy()->serverUrl().port())); + spyConnection.wait(); + if (spyConnection.count() == 0) { + return QVariant(); + } + + QSignalSpy dataSpy(socket, &QSslSocket::readyRead); + socket->write(jsonDoc.toJson(QJsonDocument::Compact) + '\n'); + // FIXME: check why it waits the full time here + dataSpy.wait(500); + if (dataSpy.count() != 1) { + qWarning() << "No data received"; + return QVariant(); + } + + QByteArray data = socket->readAll(); + socket->close(); + socket->deleteLater(); + + // Make sure the response ends with '}\n' + if (!data.endsWith("}\n")) { + qWarning() << "JSON data does not end with \"}\n\""; + return QVariant(); + } + + // Make sure the response it a valid JSON string + QJsonParseError error; + jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qWarning() << "JSON parser error" << error.errorString(); + return QVariant(); + } + + QVariantMap response = jsonDoc.toVariant().toMap(); + + if (response.value("id").toInt() == m_commandCounter) { + m_commandCounter++; + return jsonDoc.toVariant(); + } + + m_commandCounter++; + return QVariant(); +} + +QVariant BaseTest::injectTcpSocketTunnelProxyData(const QByteArray &data) +{ + QSslSocket *socket = new QSslSocket(this); + typedef void (QSslSocket:: *sslErrorsSignal)(const QList &); + QObject::connect(socket, static_cast(&QSslSocket::sslErrors), this, &BaseTest::sslSocketSslErrors); + + QSignalSpy spyConnection(socket, &QSslSocket::connected); + socket->connectToHostEncrypted(Engine::instance()->tcpSocketServerTunnelProxy()->serverUrl().host(), + static_cast(Engine::instance()->tcpSocketServerTunnelProxy()->serverUrl().port())); spyConnection.wait(); if (spyConnection.count() == 0) { return QVariant(); @@ -465,7 +668,6 @@ void BaseTest::initTestCase() "T_xh6pMkOhE6g"; qCDebug(dcApplication()) << "Init test case done."; - //restartEngine(); } void BaseTest::cleanupTestCase() diff --git a/tests/testbase/basetest.h b/tests/testbase/basetest.h index 4643272..b6d8d41 100644 --- a/tests/testbase/basetest.h +++ b/tests/testbase/basetest.h @@ -59,6 +59,9 @@ protected: QUrl m_serverUrl = QUrl("wss://127.0.0.1:1212"); QUrl m_serverUrlTcp = QUrl("ssl://127.0.0.1:1213"); + QUrl m_serverUrlTunnel = QUrl("wss://127.0.0.1:2212"); + QUrl m_serverUrlTunnelTcp = QUrl("ssl://127.0.0.1:2213"); + QSslConfiguration m_sslConfiguration; Authenticator *m_authenticator = nullptr; @@ -79,11 +82,18 @@ protected: void startServer(); void stopServer(); - QVariant invokeWebSocketApiCall(const QString &method, const QVariantMap params = QVariantMap(), bool remainsConnected = true); - QVariant injectWebSocketData(const QByteArray &data); + QVariant invokeWebSocketProxyApiCall(const QString &method, const QVariantMap params = QVariantMap(), bool remainsConnected = true); + QVariant injectWebSocketProxyData(const QByteArray &data); + + QVariant invokeTcpSocketProxyApiCall(const QString &method, const QVariantMap params = QVariantMap(), bool remainsConnected = true); + QVariant injectTcpSocketProxyData(const QByteArray &data); + + QVariant invokeWebSocketTunnelProxyApiCall(const QString &method, const QVariantMap params = QVariantMap(), bool remainsConnected = true); + QVariant injectWebSocketTunnelProxyData(const QByteArray &data); + + QVariant invokeTcpSocketTunnelProxyApiCall(const QString &method, const QVariantMap params = QVariantMap(), bool remainsConnected = true); + QVariant injectTcpSocketTunnelProxyData(const QByteArray &data); - QVariant invokeTcpSocketApiCall(const QString &method, const QVariantMap params = QVariantMap(), bool remainsConnected = true); - QVariant injectTcpSocketData(const QByteArray &data); bool createRemoteConnection(const QString &token, const QString &nonce, QObject *parent); diff --git a/tests/tests.pro b/tests/tests.pro index b1c1bc0..7d077d8 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -1,10 +1,10 @@ include(../nymea-remoteproxy.pri) TEMPLATE=subdirs -SUBDIRS += test-offline +SUBDIRS += test-proxy test-tunnelproxy -online { - message("Online tests enabled") - SUBDIRS += test-online -} +#online { +# message("Online tests enabled") +# SUBDIRS += test-online +#}