From 033de0e20ad14ee04645581fce0ad58202f8ec95 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 5 Feb 2023 23:25:10 +0100 Subject: [PATCH] Validate plugin entrypoint Check that Master/Instance/Control plugins export the correct class and that this class is a subclass of the correct base class. This gives a better user experience when plugins are incorrectly set up and loads a duplicated clusterio/lib, and when accidentally using the wrong name when developing plugins. --- packages/lib/plugin_loader.js | 26 +++++++++++++++++----- test/lib/plugin_loader.js | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/packages/lib/plugin_loader.js b/packages/lib/plugin_loader.js index f06e332d..b5d38eeb 100644 --- a/packages/lib/plugin_loader.js +++ b/packages/lib/plugin_loader.js @@ -6,6 +6,7 @@ "use strict"; const libErrors = require("./errors"); +const libPlugin = require("./plugin"); const path = require("path"); @@ -48,6 +49,22 @@ async function loadPluginInfos(pluginList) { return plugins; } +function loadPluginClass(entrypointName, className, pluginClass, pluginInfo) { + let resolvedPath = path.posix.join(pluginInfo.requirePath, pluginInfo[entrypointName]); + let entrypoint = require(resolvedPath); + if (!entrypoint[className]) { + throw new libErrors.PluginError(pluginInfo.name, + new Error(`Expected ${resolvedPath} to export a class named ${className}`) + ); + } + if (!(entrypoint[className].prototype instanceof pluginClass)) { + throw new libErrors.PluginError(pluginInfo.name, + new Error(`Expected ${className} exported from ${resolvedPath} to be a subclass of ${pluginClass.name}`) + ); + } + return entrypoint[className]; +} + /** * Load master plugin class of a plugin * @@ -58,8 +75,7 @@ async function loadPluginInfos(pluginList) { * @static */ async function loadMasterPluginClass(pluginInfo) { - let entrypoint = require(path.posix.join(pluginInfo.requirePath, pluginInfo.masterEntrypoint)); - return entrypoint.MasterPlugin; + return loadPluginClass("masterEntrypoint", "MasterPlugin", libPlugin.BaseMasterPlugin, pluginInfo); } /** @@ -72,8 +88,7 @@ async function loadMasterPluginClass(pluginInfo) { * @static */ async function loadInstancePluginClass(pluginInfo) { - let entrypoint = require(path.posix.join(pluginInfo.requirePath, pluginInfo.instanceEntrypoint)); - return entrypoint.InstancePlugin; + return loadPluginClass("instanceEntrypoint", "InstancePlugin", libPlugin.BaseInstancePlugin, pluginInfo); } /** @@ -86,8 +101,7 @@ async function loadInstancePluginClass(pluginInfo) { * @static */ async function loadControlPluginClass(pluginInfo) { - let entrypoint = require(path.posix.join(pluginInfo.requirePath, pluginInfo.controlEntrypoint)); - return entrypoint.ControlPlugin; + return loadPluginClass("controlEntrypoint", "ControlPlugin", libPlugin.BaseControlPlugin, pluginInfo); } module.exports = { diff --git a/test/lib/plugin_loader.js b/test/lib/plugin_loader.js index d1153c1b..d8ec95e9 100644 --- a/test/lib/plugin_loader.js +++ b/test/lib/plugin_loader.js @@ -64,4 +64,45 @@ describe("lib/plugin_loader", function() { ); }); }); + describe("loadMasterPluginClass()", function() { + let baseDir = path.join("temp", "test", "plugin"); + let missingClass = path.join(baseDir, "missing_class_plugin"); + let wrongParentClass = path.join(baseDir, "wrong_parent_class_plugin"); + before(async function() { + async function writeEntrypoint(pluginPath, content) { + await fs.outputFile(path.join(pluginPath, "master.js"), content); + } + + await writeEntrypoint(missingClass, ""); + await writeEntrypoint(wrongParentClass, "class MasterPlugin {}\n module.exports = { MasterPlugin };\n"); + }); + it("should throw if class is missing from entrypoint", async function() { + await assert.rejects( + libPluginLoader.loadMasterPluginClass({ + name: "test", + masterEntrypoint: "master", + requirePath: path.resolve(missingClass), + }), + { + message: + `PluginError: Expected ${path.resolve(missingClass, "master")} ` + + "to export a class named MasterPlugin", + } + ); + }); + it("should throw if class is not a subclass of BaseMasterPlugin", async function() { + await assert.rejects( + libPluginLoader.loadMasterPluginClass({ + name: "test", + masterEntrypoint: "master", + requirePath: path.resolve(wrongParentClass), + }), + { + message: + "PluginError: Expected MasterPlugin exported from " + + `${path.resolve(wrongParentClass, "master")} to be a subclass of BaseMasterPlugin`, + } + ); + }); + }); });