diff --git a/data/assets/util/webgui.asset b/data/assets/util/webgui.asset index 014054fc1b..bee410ad17 100644 --- a/data/assets/util/webgui.asset +++ b/data/assets/util/webgui.asset @@ -1,8 +1,8 @@ local guiCustomization = asset.require('customization/gui') -- Select which commit hashes to use for the frontend and backend -local frontendHash = "f6db6a5dd79df1e310a75d50f8b1f97aea8a9596" -local backendHash = "3e862a67ff2869d83187ad8bda05746b583d13d4" +local frontendHash = "abf5fe23ef29af408d6c071057f1cc706c9b09a3" +local backendHash = "6e773425b3e90ba93f0090e44427e474fe5c633f" local dataProvider = "data.openspaceproject.com/files/webgui" @@ -10,7 +10,7 @@ local backend = asset.syncedResource({ Identifier = "WebGuiBackend", Name = "Web Gui Backend", Type = "UrlSynchronization", - Url = dataProvider .. "/backend/" .. backendHash .. "/backend.js" + Url = dataProvider .. "/backend/" .. backendHash .. "/backend.zip" }) local frontend = asset.syncedResource({ @@ -26,14 +26,21 @@ asset.onInitialize(function () if not openspace.directoryExists(dest) then openspace.unzipFile(frontend .. "/frontend.zip", dest, true) end + + -- Unzip the frontend bundle + dest = backend .. "/backend" + if not openspace.directoryExists(dest) then + openspace.unzipFile(backend .. "/backend.zip", dest, true) + end + -- Do not serve the files if we are in webgui development mode. -- In that case, you have to serve the webgui manually, using `npm start`. if not guiCustomization.webguiDevelopmentMode then openspace.setPropertyValueSingle( - "Modules.WebGui.ServerProcessEntryPoint", backend .. "/backend.js" + "Modules.WebGui.ServerProcessEntryPoint", backend .. "/backend/backend.js" ) openspace.setPropertyValueSingle( - "Modules.WebGui.ServerProcessWorkingDirectory", frontend .. "/frontend" + "Modules.WebGui.WebDirectory", frontend .. "/frontend" ) openspace.setPropertyValueSingle("Modules.WebGui.ServerProcessEnabled", true) end diff --git a/ext/ghoul b/ext/ghoul index 655809e25c..5e70561a05 160000 --- a/ext/ghoul +++ b/ext/ghoul @@ -1 +1 @@ -Subproject commit 655809e25c60ad7d17141e68684eaaae0f47acc3 +Subproject commit 5e70561a05142c6bbb2484399b6816f61ce01bf7 diff --git a/include/openspace/engine/configuration.h b/include/openspace/engine/configuration.h index afa255a410..ce47681226 100644 --- a/include/openspace/engine/configuration.h +++ b/include/openspace/engine/configuration.h @@ -105,10 +105,6 @@ struct Configuration { }; OpenGLDebugContext openGLDebugContext; - std::string serverPasskey = "17308"; - bool doesRequireSocketAuthentication = true; - std::vector clientAddressWhitelist = {}; - struct HTTPProxy { bool usingHttpProxy = false; std::string address; diff --git a/modules/cefwebgui/cefwebguimodule.cpp b/modules/cefwebgui/cefwebguimodule.cpp index f600b1bf0e..6d649a95cd 100644 --- a/modules/cefwebgui/cefwebguimodule.cpp +++ b/modules/cefwebgui/cefwebguimodule.cpp @@ -22,9 +22,10 @@ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************************/ -#include - #include + +#include +#include #include #include #include @@ -131,7 +132,14 @@ void CefWebGuiModule::internalInitialize(const ghoul::Dictionary& configuration) } }); - _url = configuration.value(GuiUrlInfo.identifier); + + if (configuration.hasValue(GuiUrlInfo.identifier)) { + _url = configuration.value(GuiUrlInfo.identifier); + } else { + WebGuiModule* webGuiModule = global::moduleEngine.module(); + _url = "http://localhost:" + + std::to_string(webGuiModule->port()) + "/#/onscreen"; + } _enabled = configuration.hasValue(EnabledInfo.identifier) && configuration.value(EnabledInfo.identifier); diff --git a/modules/server/CMakeLists.txt b/modules/server/CMakeLists.txt index 4c413b5ad1..08132818cc 100644 --- a/modules/server/CMakeLists.txt +++ b/modules/server/CMakeLists.txt @@ -30,6 +30,7 @@ set(HEADER_FILES include/connection.h include/connectionpool.h include/jsonconverters.h + include/serverinterface.h include/topics/authorizationtopic.h include/topics/bouncetopic.h include/topics/getpropertytopic.h @@ -49,6 +50,7 @@ set(SOURCE_FILES src/connection.cpp src/connectionpool.cpp src/jsonconverters.cpp + src/serverinterface.cpp src/topics/authorizationtopic.cpp src/topics/bouncetopic.cpp src/topics/getpropertytopic.cpp diff --git a/modules/server/include/connection.h b/modules/server/include/connection.h index 1c86f69600..b67653310a 100644 --- a/modules/server/include/connection.h +++ b/modules/server/include/connection.h @@ -41,7 +41,12 @@ class Topic; class Connection { public: - Connection(std::unique_ptr s, std::string address); + Connection( + std::unique_ptr s, + std::string address, + bool authorized = false, + const std::string& password = "" + ); void handleMessage(const std::string& message); void sendMessage(const std::string& message); @@ -62,12 +67,9 @@ private: std::thread _thread; std::string _address; - bool _requireAuthorization; bool _isAuthorized = false; std::map _messageQueue; std::map _sentMessages; - - bool isWhitelisted() const; }; } // namespace openspace diff --git a/modules/server/include/serverinterface.h b/modules/server/include/serverinterface.h new file mode 100644 index 0000000000..eeac3aacf6 --- /dev/null +++ b/modules/server/include/serverinterface.h @@ -0,0 +1,84 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_SERVER___SERVERINTERFACE___H__ +#define __OPENSPACE_MODULE_SERVER___SERVERINTERFACE___H__ + +#include +#include +#include +#include +#include +#include + +namespace ghoul::io { class SocketServer; } + +namespace openspace { + +class ServerInterface : public properties::PropertyOwner { +public: + static std::unique_ptr createFromDictionary( + const ghoul::Dictionary& dictionary); + + ServerInterface(const ghoul::Dictionary& dictionary); + ~ServerInterface(); + + void initialize(); + void deinitialize(); + bool isEnabled() const; + bool isActive() const; + int port() const; + std::string password() const; + bool clientHasAccessWithoutPassword(const std::string& address) const; + bool clientIsBlocked(const std::string& address) const; + + ghoul::io::SocketServer* server(); + +private: + enum class InterfaceType : int { + TcpSocket = 0, + WebSocket + }; + + enum class Access : int { + Deny = 0, + RequirePassword, + Allow + }; + + properties::OptionProperty _type; + properties::IntProperty _port; + properties::BoolProperty _enabled; + properties::StringListProperty _allowAddresses; + properties::StringListProperty _requirePasswordAddresses; + properties::StringListProperty _denyAddresses; + properties::OptionProperty _defaultAccess; + properties::StringProperty _password; + + std::unique_ptr _socketServer; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_SERVER___SERVERINTERFACE___H__ diff --git a/modules/server/include/topics/authorizationtopic.h b/modules/server/include/topics/authorizationtopic.h index 447e18d3f6..b8ae341199 100644 --- a/modules/server/include/topics/authorizationtopic.h +++ b/modules/server/include/topics/authorizationtopic.h @@ -31,7 +31,7 @@ namespace openspace { class AuthorizationTopic : public Topic { public: - AuthorizationTopic() = default; + AuthorizationTopic(std::string password); void handleJson(const nlohmann::json& json) override; bool isDone() const override; @@ -39,6 +39,7 @@ public: private: bool authorize(const std::string& key); + std::string _password; bool _isAuthenticated = false; }; diff --git a/modules/server/servermodule.cpp b/modules/server/servermodule.cpp index b488c09071..89d4cfa922 100644 --- a/modules/server/servermodule.cpp +++ b/modules/server/servermodule.cpp @@ -24,6 +24,7 @@ #include +#include #include #include #include @@ -37,53 +38,102 @@ namespace { constexpr const char* _loggerCat = "ServerModule"; + constexpr const char* KeyInterfaces = "Interfaces"; } // namespace namespace openspace { -ServerModule::ServerModule() : OpenSpaceModule(ServerModule::Name) {} +ServerModule::ServerModule() + : OpenSpaceModule(ServerModule::Name) + , _interfaceOwner({"Interfaces", "Interfaces", "Server Interfaces"}) +{ + addPropertySubOwner(_interfaceOwner); +} ServerModule::~ServerModule() { disconnectAll(); cleanUpFinishedThreads(); } -void ServerModule::internalInitialize(const ghoul::Dictionary&) { +ServerInterface* ServerModule::serverInterfaceByIdentifier(const std::string& identifier) +{ + const auto si = std::find_if( + _interfaces.begin(), + _interfaces.end(), + [identifier](std::unique_ptr& i) { + return i->identifier() == identifier; + } + ); + if (si == _interfaces.end()) { + return nullptr; + } + return si->get(); +} + +void ServerModule::internalInitialize(const ghoul::Dictionary& configuration) { using namespace ghoul::io; - std::unique_ptr tcpServer = std::make_unique(); - std::unique_ptr wsServer = std::make_unique(); + if (configuration.hasValue(KeyInterfaces)) { + ghoul::Dictionary interfaces = + configuration.value(KeyInterfaces); - // Temporary hard coded addresses and ports. - tcpServer->listen("localhost", 8000); - wsServer->listen("localhost", 8001); - LDEBUG(fmt::format( - "TCP Server listening on {}:{}",tcpServer->address(), tcpServer->port() - )); + for (std::string& key : interfaces.keys()) { + if (!interfaces.hasValue(key)) { + continue; + } + ghoul::Dictionary interfaceDictionary = + interfaces.value(key); - LDEBUG(fmt::format( - "WS Server listening on {}:{}", wsServer->address(), wsServer->port() - )); + std::unique_ptr serverInterface = + ServerInterface::createFromDictionary(interfaceDictionary); - _servers.push_back(std::move(tcpServer)); - _servers.push_back(std::move(wsServer)); + serverInterface->initialize(); + + _interfaceOwner.addPropertySubOwner(serverInterface.get()); + + if (serverInterface) { + _interfaces.push_back(std::move(serverInterface)); + } + } + + } global::callback::preSync.emplace_back([this]() { preSync(); }); } void ServerModule::preSync() { // Set up new connections. - for (std::unique_ptr& server : _servers) { + for (std::unique_ptr& serverInterface : _interfaces) { + if (!serverInterface->isEnabled()) { + continue; + } + + ghoul::io::SocketServer* socketServer = serverInterface->server(); + + if (!socketServer) { + continue; + } + std::unique_ptr socket; - while ((socket = server->nextPendingSocket())) { + while ((socket = socketServer->nextPendingSocket())) { + std::string address = socket->address(); + if (serverInterface->clientIsBlocked(address)) { + // Drop connection if the address is blocked. + continue; + } socket->startStreams(); std::shared_ptr connection = std::make_shared( std::move(socket), - server->address() + address, + false, + serverInterface->password() ); connection->setThread(std::thread( [this, connection] () { handleConnection(connection); } )); + if (serverInterface->clientHasAccessWithoutPassword(address)) { + connection->setAuthorized(true); + } _connections.push_back({ std::move(connection), false }); } } @@ -115,6 +165,10 @@ void ServerModule::cleanUpFinishedThreads() { } void ServerModule::disconnectAll() { + for (std::unique_ptr& serverInterface : _interfaces) { + serverInterface->deinitialize(); + } + for (ConnectionData& connectionData : _connections) { Connection& connection = *connectionData.connection; if (connection.socket() && connection.socket()->isConnected()) { diff --git a/modules/server/servermodule.h b/modules/server/servermodule.h index b5fec25826..529355ed47 100644 --- a/modules/server/servermodule.h +++ b/modules/server/servermodule.h @@ -27,12 +27,12 @@ #include +#include + #include #include #include -namespace ghoul::io { class SocketServer; } - namespace openspace { constexpr int SOCKET_API_VERSION_MAJOR = 0; @@ -53,6 +53,8 @@ public: ServerModule(); virtual ~ServerModule(); + ServerInterface* serverInterfaceByIdentifier(const std::string& identifier); + protected: void internalInitialize(const ghoul::Dictionary& configuration) override; @@ -72,7 +74,8 @@ private: std::deque _messageQueue; std::vector _connections; - std::vector> _servers; + std::vector> _interfaces; + properties::PropertyOwner _interfaceOwner; }; } // namespace openspace diff --git a/modules/server/src/connection.cpp b/modules/server/src/connection.cpp index 24243c857b..728afab4dd 100644 --- a/modules/server/src/connection.cpp +++ b/modules/server/src/connection.cpp @@ -64,13 +64,23 @@ namespace { namespace openspace { -Connection::Connection(std::unique_ptr s, std::string address) +Connection::Connection(std::unique_ptr s, + std::string address, + bool authorized, + const std::string& password) : _socket(std::move(s)) , _address(std::move(address)) + , _isAuthorized(authorized) { ghoul_assert(_socket, "Socket must not be nullptr"); - _topicFactory.registerClass(AuthenticationTopicKey); + _topicFactory.registerClass( + AuthenticationTopicKey, + [password](bool useDictionary, const ghoul::Dictionary& dict) { + return new AuthorizationTopic(password); + } + ); + _topicFactory.registerClass(GetPropertyTopicKey); _topicFactory.registerClass(LuaScriptTopicKey); _topicFactory.registerClass(SetPropertyTopicKey); @@ -80,9 +90,6 @@ Connection::Connection(std::unique_ptr s, std::string address _topicFactory.registerClass(TriggerPropertyTopicKey); _topicFactory.registerClass(BounceTopicKey); _topicFactory.registerClass(VersionTopicKey); - - // see if the default config for requiring auth (on) is overwritten - _requireAuthorization = global::configuration.doesRequireSocketAuthentication; } void Connection::handleMessage(const std::string& message) { @@ -186,8 +193,7 @@ void Connection::sendJson(const nlohmann::json& json) { } bool Connection::isAuthorized() const { - // require either auth to be disabled or client to be authenticated - return !_requireAuthorization || isWhitelisted() || _isAuthorized; + return _isAuthorized; } void Connection::setThread(std::thread&& thread) { @@ -206,9 +212,4 @@ void Connection::setAuthorized(bool status) { _isAuthorized = status; } -bool Connection::isWhitelisted() const { - const std::vector& wl = global::configuration.clientAddressWhitelist; - return std::find(wl.begin(), wl.end(), _address) != wl.end(); -} - } // namespace openspace diff --git a/modules/server/src/serverinterface.cpp b/modules/server/src/serverinterface.cpp new file mode 100644 index 0000000000..cbc2cadfc4 --- /dev/null +++ b/modules/server/src/serverinterface.cpp @@ -0,0 +1,262 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include +#include + +#include +#include + +namespace { + + constexpr const char* KeyIdentifier = "Identifier"; + constexpr const char* TcpSocketType = "TcpSocket"; + constexpr const char* WebSocketType = "WebSocket"; + constexpr const char* DenyAccess = "Deny"; + constexpr const char* RequirePassword = "RequirePassword"; + constexpr const char* AllowAccess = "Allow"; + + constexpr openspace::properties::Property::PropertyInfo EnabledInfo = { + "Enabled", + "Is Enabled", + "This setting determines whether this server interface is enabled or not." + }; + + constexpr openspace::properties::Property::PropertyInfo TypeInfo = { + "Type", + "Type", + "Whether the interface is using a Socket or a WebSocket" + }; + + constexpr openspace::properties::Property::PropertyInfo PortInfo = { + "Port", + "Port", + "The network port to use for this sevrer interface" + }; + + constexpr openspace::properties::Property::PropertyInfo DefaultAccessInfo = { + "DefaultAccess", + "Default Access", + "Sets the default access policy: Allow, RequirePassword or Deny" + }; + + constexpr openspace::properties::Property::PropertyInfo AllowAddressesInfo = { + "AllowAddresses", + "Allow Addresses", + "Ip addresses or domains that should always be allowed access to this interface" + }; + + constexpr openspace::properties::Property::PropertyInfo RequirePasswordAddressesInfo = { + "RequirePasswordAddresses", + "Require Password Addresses", + "Ip addresses or domains that should be allowed access if they provide a password" + }; + + constexpr openspace::properties::Property::PropertyInfo DenyAddressesInfo = { + "DenyAddresses", + "Deny Addresses", + "Ip addresses or domains that should never be allowed access to this interface" + }; + + constexpr openspace::properties::Property::PropertyInfo PasswordInfo = { + "Password", + "Password", + "Password for connecting to this interface" + }; +} + +namespace openspace { + +std::unique_ptr ServerInterface::createFromDictionary( + const ghoul::Dictionary& config) +{ + // TODO: Use documentation to verify dictionary + std::unique_ptr si = std::make_unique(config); + return std::move(si); +} + +ServerInterface::ServerInterface(const ghoul::Dictionary& config) + : properties::PropertyOwner({ "", "", "" }) + , _type(TypeInfo) + , _port(PortInfo, 0) + , _defaultAccess(DefaultAccessInfo) + , _allowAddresses(AllowAddressesInfo) + , _requirePasswordAddresses(RequirePasswordAddressesInfo) + , _denyAddresses(DenyAddressesInfo) + , _enabled(EnabledInfo) + , _password(PasswordInfo) +{ + + _type.addOption(static_cast(InterfaceType::TcpSocket), TcpSocketType); + _type.addOption(static_cast(InterfaceType::WebSocket), WebSocketType); + + _defaultAccess.addOption(static_cast(Access::Deny), DenyAccess); + _defaultAccess.addOption(static_cast(Access::RequirePassword), RequirePassword); + _defaultAccess.addOption(static_cast(Access::Allow), AllowAccess); + + const std::string identifier = config.value(KeyIdentifier); + + std::function readList = + [config](const std::string& key, properties::StringListProperty& list) { + if (config.hasValue(key)) { + const ghoul::Dictionary& dict = config.value(key); + std::vector v; + for (const std::string& k : dict.keys()) { + v.push_back(dict.value(k)); + } + list.set(v); + } + }; + + readList(AllowAddressesInfo.identifier, _allowAddresses); + readList(DenyAddressesInfo.identifier, _denyAddresses); + readList(RequirePasswordAddressesInfo.identifier, _requirePasswordAddresses); + + this->setIdentifier(identifier); + this->setGuiName(identifier); + this->setDescription("Settings for server interface " + identifier); + + const std::string type = config.value(TypeInfo.identifier); + if (type == TcpSocketType) { + _type = static_cast(InterfaceType::TcpSocket); + } else if (type == WebSocketType) { + _type = static_cast(InterfaceType::WebSocket); + } + + if (config.hasValue(PasswordInfo.identifier)) { + _password = config.value(PasswordInfo.identifier); + } + + _port = static_cast(config.value(PortInfo.identifier)); + _enabled = config.value(EnabledInfo.identifier); + + std::function reinitialize = [this]() { + deinitialize(); + initialize(); + }; + + _type.onChange(reinitialize); + _port.onChange(reinitialize); + _enabled.onChange(reinitialize); + _defaultAccess.onChange(reinitialize); + _allowAddresses.onChange(reinitialize); + _requirePasswordAddresses.onChange(reinitialize); + _denyAddresses.onChange(reinitialize); + + addProperty(_type); + addProperty(_port); + addProperty(_enabled); + addProperty(_defaultAccess); + addProperty(_allowAddresses); + addProperty(_requirePasswordAddresses); + addProperty(_denyAddresses); + addProperty(_password); +} + +ServerInterface::~ServerInterface() {} + +void ServerInterface::initialize() { + if (!_enabled) { + return; + } + switch (static_cast(_type.value())) { + case InterfaceType::TcpSocket: + _socketServer = std::make_unique(); + break; + case InterfaceType::WebSocket: + _socketServer = std::make_unique(); + break; + } + _socketServer->listen(_port); +} + +void ServerInterface::deinitialize() { + _socketServer->close(); +} + +bool ServerInterface::isEnabled() const { + return _enabled.value(); +} + +bool ServerInterface::isActive() const { + return _socketServer->isListening(); +} + +int ServerInterface::port() const { + return _port; +} + +std::string ServerInterface::password() const { + return _password; +} + +bool ServerInterface::clientHasAccessWithoutPassword( + const std::string& clientAddress) const +{ + for (const std::string& address : _allowAddresses.value()) { + if (clientAddress == address) { + return true; + } + } + Access access = static_cast(_defaultAccess.value()); + if (access == Access::Allow) { + for (const std::string& address : _denyAddresses.value()) { + if (clientAddress == address) { + return false; + } + } + return true; + } + return false; +} + +bool ServerInterface::clientIsBlocked(const std::string& clientAddress) const { + for (const std::string& address : _denyAddresses.value()) { + if (clientAddress == address) { + return true; + } + } + Access access = static_cast(_defaultAccess.value()); + if (access == Access::Deny) { + for (const std::string& address : _allowAddresses.value()) { + if (clientAddress == address) { + return false; + } + } + for (const std::string& address : _requirePasswordAddresses.value()) { + if (clientAddress == address) { + return false; + } + } + return true; + } + return false; +} + +ghoul::io::SocketServer* ServerInterface::server() { + return _socketServer.get(); +} + + +} diff --git a/modules/server/src/topics/authorizationtopic.cpp b/modules/server/src/topics/authorizationtopic.cpp index d3e17b6845..3781b0cc3d 100644 --- a/modules/server/src/topics/authorizationtopic.cpp +++ b/modules/server/src/topics/authorizationtopic.cpp @@ -32,59 +32,45 @@ namespace { constexpr const char* _loggerCat = "AuthorizationTopic"; - /* https://httpstatuses.com/ */ - enum class StatusCode : int { - OK = 200, - Accepted = 202, - - BadRequest = 400, - Unauthorized = 401, - NotAcceptable = 406, - - NotImplemented = 501 - }; - - - - nlohmann::json message(const std::string& message, StatusCode statusCode) { - return { { "message", message }, { "code", static_cast(statusCode) } }; - } - + constexpr const char* KeyStatus = "status"; + constexpr const char* Authorized = "authorized"; + constexpr const char* IncorrectKey = "incorrectKey"; + constexpr const char* BadRequest = "badRequest"; } // namespace namespace openspace { +AuthorizationTopic::AuthorizationTopic(std::string password) + : _password(std::move(password)) +{} + bool AuthorizationTopic::isDone() const { return _isAuthenticated; } void AuthorizationTopic::handleJson(const nlohmann::json& json) { if (isDone()) { - _connection->sendJson(message("Already authorized.", StatusCode::OK)); + _connection->sendJson(wrappedPayload({ KeyStatus, Authorized })); } else { try { auto providedKey = json.at("key").get(); if (authorize(providedKey)) { - _connection->sendJson(message("Authorization OK.", StatusCode::Accepted)); _connection->setAuthorized(true); + _connection->sendJson(wrappedPayload({ KeyStatus, Authorized })); LINFO("Client successfully authorized."); } else { - _connection->sendJson(message("Invalid key", StatusCode::NotAcceptable)); + _connection->sendJson(wrappedPayload({ KeyStatus, IncorrectKey })); } } catch (const std::out_of_range&) { - _connection->sendJson( - message("Invalid request, key must be provided.", StatusCode::BadRequest) - ); + _connection->sendJson(wrappedPayload({ KeyStatus, BadRequest })); } catch (const std::domain_error&) { - _connection->sendJson( - message("Invalid request, invalid key format.", StatusCode::BadRequest) - ); + _connection->sendJson(wrappedPayload({ KeyStatus, BadRequest })); } } } bool AuthorizationTopic::authorize(const std::string& key) { - _isAuthenticated = (key == global::configuration.serverPasskey); + _isAuthenticated = (key == _password); return _isAuthenticated; } diff --git a/modules/webgui/webguimodule.cpp b/modules/webgui/webguimodule.cpp index 3cb740fb7d..ac5a1bcabf 100644 --- a/modules/webgui/webguimodule.cpp +++ b/modules/webgui/webguimodule.cpp @@ -24,6 +24,9 @@ #include +#include +#include +#include #include #include #include @@ -38,20 +41,41 @@ namespace { "Enable the node js based process used to serve the Web GUI." }; + constexpr openspace::properties::Property::PropertyInfo AddressInfo = { + "Address", + "Address", + "The network address to use when connecting to OpenSpace from the Web GUI." + }; + + constexpr openspace::properties::Property::PropertyInfo PortInfo = { + "Port", + "Port", + "The network port to use when serving the Web GUI over HTTP." + }; + + constexpr openspace::properties::Property::PropertyInfo WebSocketInterfaceInfo = { + "WebSocketInterface", + "WebSocket Interface", + "The identifier of the websocket interface to use when communicating." + }; + constexpr openspace::properties::Property::PropertyInfo ServerProcessEntryPointInfo = { "ServerProcessEntryPoint", "Server Process Entry Point", - "The node js command to invoke" + "The node js command to invoke." }; constexpr openspace::properties::Property::PropertyInfo - ServerProcessWorkingDirectoryInfo = + WebDirectoryInfo = { - "ServerProcessWorkingDirectory", - "Server Process Working Directory", - "The working directory of the process" + "WebDirectory", + "Web Directory", + "Directory from which to to serve static content", }; + + constexpr const char* DefaultAddress = "localhost"; + constexpr const int DefaultPort = 4680; } namespace openspace { @@ -60,14 +84,40 @@ WebGuiModule::WebGuiModule() : OpenSpaceModule(WebGuiModule::Name) , _enabled(ServerProcessEnabledInfo, false) , _entryPoint(ServerProcessEntryPointInfo) - , _workingDirectory(ServerProcessWorkingDirectoryInfo) + , _webDirectory(WebDirectoryInfo) + , _address(AddressInfo, DefaultAddress) + , _port(PortInfo, DefaultPort) + , _webSocketInterface(WebSocketInterfaceInfo, "") { addProperty(_enabled); addProperty(_entryPoint); - addProperty(_workingDirectory); + addProperty(_webDirectory); + addProperty(_address); + addProperty(_port); } -void WebGuiModule::internalInitialize(const ghoul::Dictionary&) { +int WebGuiModule::port() const { + return _port; +} + +std::string WebGuiModule::address() const { + return _address; +} + +void WebGuiModule::internalInitialize(const ghoul::Dictionary& configuration) { + if (configuration.hasValue(PortInfo.identifier)) { + _port = configuration.value(PortInfo.identifier); + } + + if (configuration.hasValue(AddressInfo.identifier)) { + _address = configuration.value(AddressInfo.identifier); + } + + if (configuration.hasValue(WebSocketInterfaceInfo.identifier)) { + _webSocketInterface = + configuration.value(WebSocketInterfaceInfo.identifier); + } + auto startOrStop = [this]() { if (_enabled) { startProcess(); @@ -85,20 +135,39 @@ void WebGuiModule::internalInitialize(const ghoul::Dictionary&) { _enabled.onChange(startOrStop); _entryPoint.onChange(restartIfEnabled); - _workingDirectory.onChange(restartIfEnabled); + _webDirectory.onChange(restartIfEnabled); + _port.onChange(restartIfEnabled); startOrStop(); } void WebGuiModule::startProcess() { + ServerModule* serverModule = global::moduleEngine.module(); + const ServerInterface* serverInterface = + serverModule->serverInterfaceByIdentifier(_webSocketInterface); + if (!serverInterface) { + LERROR("Missing server interface. Server process could not start."); + return; + } + const int webSocketPort = serverInterface->port(); + + #ifdef _MSC_VER const std::string nodePath = absPath("${MODULE_WEBGUI}/ext/nodejs/node.exe"); #else const std::string nodePath = absPath("${MODULE_WEBGUI}/ext/nodejs/node"); #endif + const std::string command = "\"" + nodePath + "\" " + + "\"" + _entryPoint.value() + "\"" + + " --directory \"" + _webDirectory.value() + "\"" + + " --http-port \"" + std::to_string(_port.value()) + "\" " + + " --ws-address \"" + _address.value() + "\"" + + " --ws-port \"" + std::to_string(webSocketPort) + "\"" + + " --auto-close --local"; + _process = std::make_unique( - "\"" + nodePath + "\" \"" + _entryPoint.value() + "\"", - _workingDirectory.value(), + command, + _webDirectory.value(), [](const char* data, size_t n) { const std::string str(data, n); LDEBUG(fmt::format("Web GUI server output: {}", str)); diff --git a/modules/webgui/webguimodule.h b/modules/webgui/webguimodule.h index 1458ce6dd1..8e6de28a80 100644 --- a/modules/webgui/webguimodule.h +++ b/modules/webgui/webguimodule.h @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -38,6 +39,8 @@ class WebGuiModule : public OpenSpaceModule { public: static constexpr const char* Name = "WebGui"; WebGuiModule(); + int port() const; + std::string address() const; protected: void internalInitialize(const ghoul::Dictionary&) override; @@ -49,7 +52,11 @@ private: std::unique_ptr _process; properties::BoolProperty _enabled; properties::StringProperty _entryPoint; - properties::StringProperty _workingDirectory; + properties::StringProperty _webDirectory; + + properties::IntProperty _port; + properties::StringProperty _address; + properties::StringProperty _webSocketInterface; }; } // namespace openspace diff --git a/openspace.cfg b/openspace.cfg index 88b96514c1..5a4e163781 100644 --- a/openspace.cfg +++ b/openspace.cfg @@ -86,12 +86,41 @@ ModuleConfigurations = { "data.openspaceproject.com/request" } }, + Server = { + Interfaces = { + { + Type = "TcpSocket", + Identifier = "DefaultTcpSocketInterface", + Port = 4681, + Enabled = true, + DefaultAccess = "Deny", + AllowAddresses = {"127.0.0.1", "localhost"}, + RequirePasswordAddresses = {}, + Password = "" + }, + { + Type = "WebSocket", + Identifier = "DefaultWebSocketInterface", + Port = 4682, + Enabled = true, + DefaultAccess = "Deny", + AllowAddresses = {"127.0.0.1", "localhost"}, + RequirePasswordAddresses = {}, + Password = "" + } + } + }, WebBrowser = { Enabled = true, WebHelperLocation = "${BIN}/openspace_web_helper" }, + WebGui = { + Address = "localhost", + HttpPort = 4680, + WebSocketInterface = "DefaultWebSocketInterface" + }, CefWebGui = { - GuiUrl = "http://localhost:8080/#/onscreen/", + -- GuiUrl = "http://localhost:4680/#/onscreen/", Enabled = true, Visible = true } @@ -154,9 +183,3 @@ OpenGLDebugContext = { -- FilterSeverity = { } } --RenderingMethod = "ABuffer" -- alternative: "Framebuffer" - -ServerPasskey = "secret!" -ClientAddressWhitelist = { - "127.0.0.1", - "localhost" -} diff --git a/src/engine/configuration.cpp b/src/engine/configuration.cpp index 807322a765..3ce4aa2c1a 100644 --- a/src/engine/configuration.cpp +++ b/src/engine/configuration.cpp @@ -57,9 +57,6 @@ namespace { constexpr const char* KeyKeyboardShortcuts = "KeyboardShortcuts"; constexpr const char* KeyDocumentation = "Documentation"; constexpr const char* KeyFactoryDocumentation = "FactoryDocumentation"; - constexpr const char* KeyRequireSocketAuthentication = "RequireSocketAuthentication"; - constexpr const char* KeyServerPasskey = "ServerPasskey"; - constexpr const char* KeyClientAddressWhitelist = "ClientAddressWhitelist"; constexpr const char* KeyLicenseDocumentation = "LicenseDocumentation"; constexpr const char* KeyShutdownCountdown = "ShutdownCountdown"; constexpr const char* KeyPerSceneCache = "PerSceneCache"; @@ -294,9 +291,6 @@ void parseLuaState(Configuration& configuration) { getValue(s, KeyDisableSceneOnMaster, c.isSceneTranslationOnMasterDisabled); getValue(s, KeyDisableInGameConsole, c.isConsoleDisabled); getValue(s, KeyRenderingMethod, c.renderingMethod); - getValue(s, KeyServerPasskey, c.serverPasskey); - getValue(s, KeyRequireSocketAuthentication, c.doesRequireSocketAuthentication); - getValue(s, KeyClientAddressWhitelist, c.clientAddressWhitelist); getValue(s, KeyLogging, c.logging); getValue(s, KeyDocumentation, c.documentation); diff --git a/src/engine/configuration_doc.inl b/src/engine/configuration_doc.inl index 87cb4b12c0..2f92091b2c 100644 --- a/src/engine/configuration_doc.inl +++ b/src/engine/configuration_doc.inl @@ -209,26 +209,6 @@ documentation::Documentation Configuration::Documentation = { Optional::Yes, "All documentations that are generated at application startup." }, - { - KeyRequireSocketAuthentication, - new BoolVerifier, - Optional::Yes, - "If socket connections should be authenticated or not before they are " - "allowed to get or set information. Defaults to 'true'." - }, - { - KeyServerPasskey, - new StringVerifier, - Optional::Yes, - "Passkey to limit server access. Used to authorize incoming connections." - }, - { - KeyClientAddressWhitelist, - new StringListVerifier, - Optional::Yes, - "String containing white listed client IP addresses that won't need to be" - "authorized with the server. Space separated" - }, { KeyShutdownCountdown, new DoubleGreaterEqualVerifier(0.0), diff --git a/src/network/parallelserver.cpp b/src/network/parallelserver.cpp index f6d3d3497e..f2514f604a 100644 --- a/src/network/parallelserver.cpp +++ b/src/network/parallelserver.cpp @@ -41,7 +41,7 @@ namespace openspace { void ParallelServer::start(int port, const std::string& password, const std::string& changeHostPassword) { - _socketServer.listen("localhost", port); + _socketServer.listen(port); _passwordHash = std::hash{}(password); _changeHostPasswordHash = std::hash{}(changeHostPassword);