/***************************************************************************** * Copyright (c) 2014-2023 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #pragma once #ifdef ENABLE_SCRIPTING # ifndef DISABLE_NETWORK # include "../../../Context.h" # include "../../../config/Config.h" # include "../../../network/Socket.h" # include "../../Duktape.hpp" # include "../../ScriptEngine.h" # include # include # include namespace OpenRCT2::Scripting { class EventList { private: std::vector> _listeners; std::vector& GetListenerList(uint32_t id) { if (_listeners.size() <= id) { _listeners.resize(static_cast(id) + 1); } return _listeners[id]; } public: void Raise( uint32_t id, const std::shared_ptr& plugin, const std::vector& args, bool isGameStateMutable) { auto& scriptEngine = GetContext()->GetScriptEngine(); // Use simple for i loop in case listeners is modified during the loop auto listeners = GetListenerList(id); for (size_t i = 0; i < listeners.size(); i++) { scriptEngine.ExecutePluginCall(plugin, listeners[i], args, isGameStateMutable); // Safety, listeners might get reallocated listeners = GetListenerList(id); } } void AddListener(uint32_t id, const DukValue& listener) { auto& listeners = GetListenerList(id); listeners.push_back(listener); } void RemoveListener(uint32_t id, const DukValue& value) { auto& listeners = GetListenerList(id); listeners.erase(std::remove(listeners.begin(), listeners.end(), value), listeners.end()); } void RemoveAllListeners(uint32_t id) { auto& listeners = GetListenerList(id); listeners.clear(); } }; class ScSocketBase { private: std::shared_ptr _plugin; protected: static bool IsLocalhostAddress(std::string_view s) { return s == "localhost" || s == "127.0.0.1" || s == "::"; } static bool IsOnWhiteList(std::string_view host) { constexpr char delimiter = ','; size_t start_pos = 0; size_t end_pos = 0; while ((end_pos = gConfigPlugin.AllowedHosts.find(delimiter, start_pos)) != std::string::npos) { if (host == gConfigPlugin.AllowedHosts.substr(start_pos, end_pos - start_pos)) { return true; } start_pos = end_pos + 1; } return host == gConfigPlugin.AllowedHosts.substr(start_pos, gConfigPlugin.AllowedHosts.length() - start_pos); } public: ScSocketBase(const std::shared_ptr& plugin) : _plugin(plugin) { } virtual ~ScSocketBase() { Dispose(); } const std::shared_ptr& GetPlugin() const { return _plugin; } virtual void Update() = 0; virtual void Dispose() { } virtual bool IsDisposed() const = 0; }; class ScSocket final : public ScSocketBase { private: static constexpr uint32_t EVENT_NONE = std::numeric_limits::max(); static constexpr uint32_t EVENT_CLOSE = 0; static constexpr uint32_t EVENT_DATA = 1; static constexpr uint32_t EVENT_CONNECT_ONCE = 2; static constexpr uint32_t EVENT_ERROR = 3; EventList _eventList; std::unique_ptr _socket; bool _disposed{}; bool _connecting{}; bool _wasConnected{}; public: ScSocket(const std::shared_ptr& plugin) : ScSocketBase(plugin) { } ScSocket(const std::shared_ptr& plugin, std::unique_ptr&& socket) : ScSocketBase(plugin) , _socket(std::move(socket)) { } private: ScSocket* destroy(const DukValue& error) { CloseSocket(); return this; } ScSocket* setNoDelay(bool noDelay) { if (_socket != nullptr) { _socket->SetNoDelay(noDelay); } return this; } ScSocket* connect(uint16_t port, const std::string& host, const DukValue& callback) { auto ctx = GetContext()->GetScriptEngine().GetContext(); if (_socket != nullptr) { duk_error(ctx, DUK_ERR_ERROR, "Socket has already been created."); } else if (_disposed) { duk_error(ctx, DUK_ERR_ERROR, "Socket is disposed."); } else if (_connecting) { duk_error(ctx, DUK_ERR_ERROR, "Socket is already connecting."); } else if (!IsLocalhostAddress(host) && !IsOnWhiteList(host)) { duk_error(ctx, DUK_ERR_ERROR, "For security reasons, only connecting to localhost is allowed."); } else { _socket = CreateTcpSocket(); try { _socket->ConnectAsync(host, port); _eventList.AddListener(EVENT_CONNECT_ONCE, callback); _connecting = true; } catch (const std::exception& e) { duk_error(ctx, DUK_ERR_ERROR, e.what()); } } return this; } ScSocket* end(const DukValue& data) { if (_disposed) { auto ctx = GetContext()->GetScriptEngine().GetContext(); duk_error(ctx, DUK_ERR_ERROR, "Socket is disposed."); } else if (_socket != nullptr) { if (data.type() == DukValue::Type::STRING) { write(data.as_string()); _socket->Finish(); } else { _socket->Finish(); auto ctx = GetContext()->GetScriptEngine().GetContext(); duk_error(ctx, DUK_ERR_ERROR, "Only sending strings is currently supported."); } } return this; } bool write(const std::string& data) { if (_disposed) { auto ctx = GetContext()->GetScriptEngine().GetContext(); duk_error(ctx, DUK_ERR_ERROR, "Socket is disposed."); } else if (_socket != nullptr) { try { auto sentBytes = _socket->SendData(data.c_str(), data.size()); return sentBytes != data.size(); } catch (const std::exception&) { return false; } } return false; } ScSocket* on(const std::string& eventType, const DukValue& callback) { auto eventId = GetEventType(eventType); if (eventId != EVENT_NONE) { _eventList.AddListener(eventId, callback); } return this; } ScSocket* off(const std::string& eventType, const DukValue& callback) { auto eventId = GetEventType(eventType); if (eventId != EVENT_NONE) { _eventList.RemoveListener(eventId, callback); } return this; } void CloseSocket() { if (_socket != nullptr) { _socket->Close(); _socket = nullptr; if (_wasConnected) { _wasConnected = false; RaiseOnClose(false); } } } void RaiseOnClose(bool hadError) { auto ctx = GetContext()->GetScriptEngine().GetContext(); _eventList.Raise(EVENT_CLOSE, GetPlugin(), { ToDuk(ctx, hadError) }, false); } void RaiseOnData(const std::string& data) { auto& scriptEngine = GetContext()->GetScriptEngine(); auto ctx = scriptEngine.GetContext(); _eventList.Raise(EVENT_DATA, GetPlugin(), { ToDuk(ctx, data) }, false); } uint32_t GetEventType(std::string_view name) { if (name == "close") return EVENT_CLOSE; if (name == "data") return EVENT_DATA; if (name == "error") return EVENT_ERROR; return EVENT_NONE; } public: void Update() override { if (_disposed) return; if (_socket != nullptr) { auto status = _socket->GetStatus(); if (_connecting) { if (status == SocketStatus::Connected) { _connecting = false; _wasConnected = true; _eventList.Raise(EVENT_CONNECT_ONCE, GetPlugin(), {}, false); _eventList.RemoveAllListeners(EVENT_CONNECT_ONCE); } else if (status == SocketStatus::Closed) { _connecting = false; auto& scriptEngine = GetContext()->GetScriptEngine(); auto ctx = scriptEngine.GetContext(); auto err = _socket->GetError(); if (err == nullptr) { err = ""; } auto dukErr = ToDuk(ctx, std::string_view(err)); _eventList.Raise(EVENT_ERROR, GetPlugin(), { dukErr }, true); } } else if (status == SocketStatus::Connected) { char buffer[2048]; size_t bytesRead{}; auto result = _socket->ReceiveData(buffer, sizeof(buffer), &bytesRead); switch (result) { case NetworkReadPacket::Success: RaiseOnData(std::string(buffer, bytesRead)); break; case NetworkReadPacket::NoData: break; case NetworkReadPacket::MoreData: break; case NetworkReadPacket::Disconnected: CloseSocket(); break; } } else { CloseSocket(); } } } void Dispose() override { CloseSocket(); _disposed = true; } bool IsDisposed() const override { return _disposed; } static void Register(duk_context* ctx) { dukglue_register_method(ctx, &ScSocket::destroy, "destroy"); dukglue_register_method(ctx, &ScSocket::setNoDelay, "setNoDelay"); dukglue_register_method(ctx, &ScSocket::connect, "connect"); dukglue_register_method(ctx, &ScSocket::end, "end"); dukglue_register_method(ctx, &ScSocket::write, "write"); dukglue_register_method(ctx, &ScSocket::on, "on"); dukglue_register_method(ctx, &ScSocket::off, "off"); } }; class ScListener final : public ScSocketBase { private: static constexpr uint32_t EVENT_NONE = std::numeric_limits::max(); static constexpr uint32_t EVENT_CONNECTION = 0; EventList _eventList; std::unique_ptr _socket; std::vector> _scClientSockets; bool _disposed{}; bool listening_get() { if (_socket != nullptr) { return _socket->GetStatus() == SocketStatus::Listening; } return false; } ScListener* close() { CloseSocket(); return this; } ScListener* listen(int32_t port, const DukValue& dukHost) { auto ctx = GetContext()->GetScriptEngine().GetContext(); if (_disposed) { duk_error(ctx, DUK_ERR_ERROR, "Socket is disposed."); } else { if (_socket == nullptr) { _socket = CreateTcpSocket(); } if (_socket->GetStatus() == SocketStatus::Listening) { duk_error(ctx, DUK_ERR_ERROR, "Server is already listening."); } else { if (dukHost.type() == DukValue::Type::STRING) { auto host = dukHost.as_string(); if (IsLocalhostAddress(host) || IsOnWhiteList(host)) { try { _socket->Listen(host, port); } catch (const std::exception& e) { duk_error(ctx, DUK_ERR_ERROR, e.what()); } } else { duk_error(ctx, DUK_ERR_ERROR, "For security reasons, only binding to localhost is allowed."); } } else { _socket->Listen("127.0.0.1", port); } } } return this; } ScListener* on(const std::string& eventType, const DukValue& callback) { auto eventId = GetEventType(eventType); if (eventId != EVENT_NONE) { _eventList.AddListener(eventId, callback); } return this; } ScListener* off(const std::string& eventType, const DukValue& callback) { auto eventId = GetEventType(eventType); if (eventId != EVENT_NONE) { _eventList.RemoveListener(eventId, callback); } return this; } uint32_t GetEventType(std::string_view name) { if (name == "connection") return EVENT_CONNECTION; return EVENT_NONE; } public: ScListener(const std::shared_ptr& plugin) : ScSocketBase(plugin) { } void Update() override { if (_disposed) return; if (_socket == nullptr) return; if (_socket->GetStatus() == SocketStatus::Listening) { auto client = _socket->Accept(); if (client != nullptr) { // Default to using Nagle's algorithm like node.js does client->SetNoDelay(false); auto& scriptEngine = GetContext()->GetScriptEngine(); auto clientSocket = std::make_shared(GetPlugin(), std::move(client)); scriptEngine.AddSocket(clientSocket); auto ctx = scriptEngine.GetContext(); auto dukClientSocket = GetObjectAsDukValue(ctx, clientSocket); _eventList.Raise(EVENT_CONNECTION, GetPlugin(), { dukClientSocket }, false); } } } void CloseSocket() { if (_socket != nullptr) { _socket->Close(); _socket = nullptr; } } void Dispose() override { CloseSocket(); _disposed = true; } bool IsDisposed() const override { return _disposed; } static void Register(duk_context* ctx) { dukglue_register_property(ctx, &ScListener::listening_get, nullptr, "listening"); dukglue_register_method(ctx, &ScListener::close, "close"); dukglue_register_method(ctx, &ScListener::listen, "listen"); dukglue_register_method(ctx, &ScListener::on, "on"); dukglue_register_method(ctx, &ScListener::off, "off"); } }; } // namespace OpenRCT2::Scripting # endif #endif