From fea6c1a31dab9f202787cdc5ae13ac97bd304be5 Mon Sep 17 00:00:00 2001 From: sharworange Date: Tue, 3 Mar 2026 23:49:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=96=87=E4=BB=B6=E5=A4=B9?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=92=8Csharw=E6=A0=BC=E5=BC=8F=E7=9A=84?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E8=AF=BB=E5=8F=96=20&&=20=E7=8E=B0=E8=83=BD?= =?UTF-8?q?=E5=A4=9F=E6=89=AB=E6=8F=8F=E6=8F=92=E4=BB=B6=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=A4=B9=E7=9A=84=E5=AD=90=E6=96=87=E4=BB=B6=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Modules/PluginManagement/PluginLoader.cs | 143 ++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/Modules/PluginManagement/PluginLoader.cs b/Modules/PluginManagement/PluginLoader.cs index 858cca9..5edcc85 100644 --- a/Modules/PluginManagement/PluginLoader.cs +++ b/Modules/PluginManagement/PluginLoader.cs @@ -1,3 +1,4 @@ +using System.IO.Compression; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using sharwapi.Contracts.Core; @@ -50,7 +51,7 @@ public List LoadPlugins() return loadedPlugins; } - // 遍历 plugins 目录下的所有 DLL 文件 + // 遍历 plugins 目录下的所有 DLL 文件(单文件插件) foreach (var dllPath in Directory.GetFiles(pluginsPath, "*.dll")) { try @@ -68,9 +69,149 @@ public List LoadPlugins() } } + // 遍历 .sharw 插件包,解压到 .cache 后加载 + string cacheRoot = Path.Combine(pluginsPath, ".cache"); + foreach (var sharwPath in Directory.GetFiles(pluginsPath, "*.sharw")) + { + try + { + var cacheDir = ExtractSharwToCache(sharwPath, cacheRoot); + if (cacheDir != null) + { + var plugins = LoadPluginFromDirectory(cacheDir); + loadedPlugins.AddRange(plugins); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading .sharw plugin from {SharwPath}", sharwPath); + } + } + + // 遍历 plugins 目录下的所有子目录(多文件插件,深度为 1),跳过 .cache 缓存目录 + foreach (var subDir in Directory.GetDirectories(pluginsPath) + .Where(d => !string.Equals(Path.GetFileName(d), ".cache", + StringComparison.OrdinalIgnoreCase))) + { + try + { + var plugins = LoadPluginFromDirectory(subDir); + loadedPlugins.AddRange(plugins); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading plugin from directory {SubDir}", subDir); + } + } + return loadedPlugins; } + /// + /// 从子目录加载多文件插件,返回目录中所有 IApiPlugin 实现。 + /// 优先查找与目录同名的 DLL 作为主程序集;若不存在,则逐一扫描目录内所有 DLL 文件。 + /// AssemblyDependencyResolver 会依据主程序集的 .deps.json 自动解析同目录内的依赖。 + /// + /// 插件子目录路径 + /// 加载到的所有插件实例列表 + private List LoadPluginFromDirectory(string dirPath) + { + var result = new List(); + var dirName = Path.GetFileName(dirPath); + + // 优先约定:主 DLL 与目录名相同(例如 plugins/MyPlugin/MyPlugin.dll) + var conventionDllPath = Path.Combine(dirPath, dirName + ".dll"); + if (File.Exists(conventionDllPath)) + { + _logger.LogDebug("Loading plugin from directory {DirPath} using convention DLL {DllName}.dll", dirPath, dirName); + var plugin = LoadPluginFromPath(conventionDllPath); + if (plugin != null) result.Add(plugin); + return result; + } + + // 回退:遍历目录内所有 DLL,收集所有包含 IApiPlugin 实现的插件 + _logger.LogDebug("Convention DLL not found in {DirPath}, scanning all DLLs", dirPath); + foreach (var dllPath in Directory.GetFiles(dirPath, "*.dll")) + { + try + { + var plugin = LoadPluginFromPath(dllPath); + if (plugin != null) + { + result.Add(plugin); + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, "DLL {DllPath} in directory does not contain a valid plugin", dllPath); + } + } + + if (result.Count == 0) + { + _logger.LogWarning("No valid IApiPlugin implementation found in directory {DirPath}", dirPath); + } + + return result; + } + + /// + /// 将 .sharw 插件包解压到缓存目录,若缓存已是最新则跳过解压。 + /// 解压时执行 Zip Slip 路径合法性检查,防止恶意路径遍历攻击。 + /// + /// .sharw 文件路径 + /// 缓存根目录路径(plugins/.cache) + /// 解压后的插件缓存目录路径 + private string? ExtractSharwToCache(string sharwPath, string cacheRoot) + { + var pluginName = Path.GetFileNameWithoutExtension(sharwPath); + var cacheDir = Path.Combine(cacheRoot, pluginName); + var sharwModified = File.GetLastWriteTimeUtc(sharwPath); + + // 若缓存目录已存在且 .sharw 未更新,跳过解压 + if (Directory.Exists(cacheDir)) + { + var cacheModified = Directory.GetLastWriteTimeUtc(cacheDir); + if (sharwModified <= cacheModified) + { + _logger.LogDebug("Cache for {PluginName} is up-to-date, skipping extraction", pluginName); + return cacheDir; + } + _logger.LogInformation("Updating cache for {PluginName}", pluginName); + Directory.Delete(cacheDir, recursive: true); + } + + Directory.CreateDirectory(cacheDir); + var resolvedCache = Path.GetFullPath(cacheDir); + + using var archive = ZipFile.OpenRead(sharwPath); + foreach (var entry in archive.Entries) + { + // Zip Slip 防护:确保解压路径不超出缓存目录 + var destPath = Path.GetFullPath(Path.Combine(resolvedCache, entry.FullName)); + if (!destPath.StartsWith(resolvedCache + Path.DirectorySeparatorChar, + StringComparison.OrdinalIgnoreCase)) + { + _logger.LogWarning("Zip Slip attempt detected in {SharwPath}: {Entry}", sharwPath, entry.FullName); + throw new InvalidOperationException($"Zip Slip detected: {entry.FullName}"); + } + + if (string.IsNullOrEmpty(entry.Name)) // 目录条目 + { + Directory.CreateDirectory(destPath); + continue; + } + + Directory.CreateDirectory(Path.GetDirectoryName(destPath)!); + entry.ExtractToFile(destPath, overwrite: true); + } + + // 将缓存目录修改时间与 .sharw 同步,用于下次更新校验 + Directory.SetLastWriteTimeUtc(cacheDir, sharwModified); + _logger.LogInformation("Extracted .sharw plugin {PluginName} to {CacheDir}", pluginName, cacheDir); + return cacheDir; + } + /// /// 从指定路径加载单个插件 ///