/** * @file llscripteditorws.h * @brief WebSocket server and connection classes for external script editor integration * * $LicenseInfo:firstyear=2025&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2025, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #pragma once #include "lljsonrpcws.h" #include "llsd.h" #include "lluuid.h" #include "llhandle.h" #include "lltimer.h" #include #include #include #include // Forward declarations class LLLiveLSLEditor; class LLScriptEdContainer; class LLScriptEditorWSServer; class LLChat; class LLPanel; class LLScriptEditorWSConnection : public LLJSONRPCConnection, public std::enable_shared_from_this { public: using ptr_t = std::shared_ptr; using wptr_t = std::weak_ptr; enum DisconnectReason { REASON_NORMAL = 0, REASON_EDITOR_CLOSED = 1, REASON_PROTOCOL_ERROR = 2, REASON_TIMEOUT = 3, REASON_INTERNAL_ERROR = 4 }; LLScriptEditorWSConnection(const LLWebsocketMgr::WSServer::ptr_t server, const LLWebsocketMgr::connection_h& handle) : LLJSONRPCConnection(server, handle) { mConnectionID = sNextConnectionID++; } ~LLScriptEditorWSConnection() override = default; U32 getConnectionID() const { return mConnectionID; } // Connection lifecycle overrides void onOpen() override; void onClose() override; void sendDisconnect(S32 reason = 0, const std::string& message = "Goodbye"); private: using string_set_t = std::set; /** * @brief Handle the handshake response from the client * @param result The response data from the client containing client information */ void handleHandshakeResponse(const LLSD& result); std::string generateChallenge(); LLScriptEdContainer* getEditor() const; std::shared_ptr getServer() const; U32 mConnectionID{ 0 }; ///< Unique identifier for this connection // Client handshake response data std::string mClientName; ///< Name of the external editor client std::string mClientVersion; ///< Version of the external editor client std::string mProtocolVersion; ///< JSON-RPC protocol version supported by client std::string mScriptName; ///< Name of the script being edited std::string mScriptLanguage; ///< Programming language of the script (lsl, luau, etc.) string_set_t mLanguages; ///< Set of supported scripting languages string_set_t mFeatures; ///< Active client features (live_sync, compilation, etc.) LLUUID mChallenge; std::string mChallengeFile; ///< Temporary file used for challenge-response verification static U32 sNextConnectionID; }; /** * @class LLScriptEditorWSServer * @brief JSON-RPC 2.0 WebSocket server for external script editor integration * * This server extends the JSON-RPC server to provide specialized functionality * for external script editor integration. It manages WebSocket connections from * external script editors and provides a structured JSON-RPC 2.0 interface * between the Second Life viewer's script editing functionality and external * development tools. * * ## Architecture * * The server acts as a JSON-RPC communication hub between: * - LLLiveLSLEditor instances (in-world script editing) * - External script editors (VS Code, Atom, Sublime Text, etc.) * - Script compilation and save services * * ## Usage * * @code * // Create and start the JSON-RPC server * auto server = std::make_shared("script_editor_server", 9020); * LLWebsocketMgr::getInstance()->addServer(server); * LLWebsocketMgr::getInstance()->startServer("script_editor_server"); * * // Associate with an LSL editor * server->associateEditor(editor_handle, script_id); * @endcode * * ## Security Considerations * * - Server binds to localhost only by default for security * - JSON-RPC 2.0 structured protocol with validation * - Rate limiting handled by base JSON-RPC server * - Error handling with standardized JSON-RPC error codes */ class LLScriptEditorWSServer : public LLJSONRPCServer { public: enum SubscriptionError_t { SUBSCRIPTION_SUCCESS = 0, SUBSCRIPTION_INVALID_EDITOR, SUBSCRIPTION_INVALID_SUBSCRIPTION, SUBSCRIPTION_ALREADY_SUBSCRIBED, SUBSCRIPTION_INTERNAL_ERROR }; static constexpr char const* DEFAULT_SERVER_NAME = "script_editor_server"; static constexpr U16 DEFAULT_SERVER_PORT = 9020; using ptr_t = std::shared_ptr; using wptr_t = std::weak_ptr; LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only = true); virtual ~LLScriptEditorWSServer() = default; static LLScriptEditorWSServer::ptr_t getServer(); void onStarted() override; void onStopped() override; void onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) override; void onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t& connection) override; bool subscribeScriptEditor(const LLUUID& object_id, const LLUUID& item_id, std::string_view script_name, const LLHandle& editor_handle, const std::string &script_id); void unsubscribeEditor(const std::string &script_id); void notifyScript(const std::string& script_id, const std::string& method, const LLSD& message) const; void sendUnsubscribeScriptEditor(const std::string& script_id); void sendCompileResults(const std::string& script_id, const LLSD& results) const; LLHandle findEditorForScript(const std::string& script_id) const; void forwardChatToIDE(const LLChat& chat_msg) const; std::set getActiveScripts() const; protected: LLWebsocketMgr::WSConnection::ptr_t connectionFactory(LLWebsocketMgr::WSServer::ptr_t server, LLWebsocketMgr::connection_h handle) override; void setupConnectionMethods(LLJSONRPCConnection::ptr_t connection) override; void broadcastLanguageChange(); LLSD handleLanguageIdRequest() const; LLSD handleSyntaxRequest(const LLSD ¶ms) const; LLSD handleScriptSubscribe(U32 connection_id, const LLSD& params); LLSD handleScriptUnsubscribe(U32 connection_id, const LLSD& params); private: struct EditorSubscription { EditorSubscription(const LLUUID &object_id, const LLUUID &item_id, std::string_view script_name, LLHandle editor_handle): mObjectID(object_id), mItemID(item_id), mScriptName(script_name), mEditorHandle(editor_handle) {} U32 mConnectionID{ 0 }; LLUUID mObjectID; LLUUID mItemID; std::string mScriptName; LLScriptEditorWSConnection::wptr_t mConnection; LLHandle mEditorHandle; }; using subscriptions_t = std::unordered_map; SubscriptionError_t updateScriptSubscription(const std::string &script_id, U32 connection_id); void unsubscribeConnection(U32 connection_id); subscriptions_t mSubscriptions; std::map mActiveConnections; boost::signals2::connection mLanguageChangeSignal; LLUUID mLastSyntaxId; LLTimer mCleanupTimer; static constexpr F32 CLEANUP_INTERVAL = 60.0f; // seconds static constexpr F32 CONNECTION_TIMEOUT = 300.0f; // 5 minutes };