From eafaab9bab4ca2a54dfdc4f5eaba6bb07b2a4dfb Mon Sep 17 00:00:00 2001 From: Aigio Liu Date: Fri, 14 Jul 2023 21:11:49 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20msix=20publish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AssemblyInfo.Constants.cs | 1 + .../Plugins/Plugin.cs | 2 +- .../Plugins/Plugin.cs | 2 +- .../Plugins/Plugin.cs | 2 +- .../Plugins/Plugin.cs | 2 +- .../Plugins/Plugin.cs | 2 +- .../BD.WTTS.Client.Tools.Publish.csproj | 2 +- .../Commands/IDotNetPublishCommand.cs | 53 ++- .../Commands/IGenerateCaCertCommand.cs | 90 +++++ src/BD.WTTS.Client.Tools.Publish/Constants.cs | 33 ++ .../Helpers/MSIXHelper.cs | 309 +++++++++++++----- src/BD.WTTS.Client.Tools.Publish/Program.cs | 2 + .../Helpers/ReflectionHelper.cs | 43 +++ .../Abstractions/IPlugin.Properties.cs | 5 + .../Abstractions/PluginBase.Properties.cs | 186 ++++++++--- src/BD.WTTS.Client/Plugins/PluginsCore.cs | 100 +++--- src/BD.WTTS.Client/Startup/Startup.cs | 1 + 17 files changed, 628 insertions(+), 207 deletions(-) create mode 100644 src/BD.WTTS.Client.Tools.Publish/Commands/IGenerateCaCertCommand.cs create mode 100644 src/BD.WTTS.Client/Helpers/ReflectionHelper.cs diff --git a/src/AssemblyInfo.Constants.cs b/src/AssemblyInfo.Constants.cs index b240a4cdf..31d7d90f4 100644 --- a/src/AssemblyInfo.Constants.cs +++ b/src/AssemblyInfo.Constants.cs @@ -87,6 +87,7 @@ false public const string GameListId = "00000000-0000-0000-0000-000000000003"; public const string ArchiSteamFarmPlus = "ArchiSteamFarmPlus"; + public const string ArchiSteamFarmPlusId = "00000000-0000-0000-0000-000000000004"; public const string Authenticator = "Authenticator"; public const string AuthenticatorId = "00000000-0000-0000-0000-000000000005"; diff --git a/src/BD.WTTS.Client.Plugins.Accelerator/Plugins/Plugin.cs b/src/BD.WTTS.Client.Plugins.Accelerator/Plugins/Plugin.cs index d65c41dc6..4bf13f05f 100644 --- a/src/BD.WTTS.Client.Plugins.Accelerator/Plugins/Plugin.cs +++ b/src/BD.WTTS.Client.Plugins.Accelerator/Plugins/Plugin.cs @@ -18,7 +18,7 @@ public sealed class Plugin : PluginBase, IPlugin public sealed override string Description => "提供一些游戏相关网站服务的加速及脚本注入功能。"; - public sealed override string Author => "Steam++ 官方"; + protected sealed override string? AuthorOriginalString => null; public sealed override string? Icon => "avares://BD.WTTS.Client.Plugins.Accelerator/UI/Assets/accelerator.ico"; diff --git a/src/BD.WTTS.Client.Plugins.Authenticator/Plugins/Plugin.cs b/src/BD.WTTS.Client.Plugins.Authenticator/Plugins/Plugin.cs index 55d9f7b57..815a94774 100644 --- a/src/BD.WTTS.Client.Plugins.Authenticator/Plugins/Plugin.cs +++ b/src/BD.WTTS.Client.Plugins.Authenticator/Plugins/Plugin.cs @@ -17,7 +17,7 @@ public sealed class Plugin : PluginBase, IPlugin public sealed override string Description => "提供多平台多账号令牌管理、加密、确认交易、云同步等功能"; - public sealed override string Author => "Steam++ 官方"; + protected sealed override string? AuthorOriginalString => null; public sealed override string? Icon => "avares://BD.WTTS.Client.Plugins.Authenticator/UI/Assets/authenticator.ico"; diff --git a/src/BD.WTTS.Client.Plugins.GameAccount/Plugins/Plugin.cs b/src/BD.WTTS.Client.Plugins.GameAccount/Plugins/Plugin.cs index 3ebdc2b1c..72ac36d5b 100644 --- a/src/BD.WTTS.Client.Plugins.GameAccount/Plugins/Plugin.cs +++ b/src/BD.WTTS.Client.Plugins.GameAccount/Plugins/Plugin.cs @@ -18,7 +18,7 @@ public sealed class Plugin : PluginBase, IPlugin public sealed override string Description => "可支持自行添加多平台账号快速切换功能,Steam 可自动读取账号信息,其它平台请手动添加账号信息。"; - public sealed override string Author => "Steam++ 官方"; + protected sealed override string? AuthorOriginalString => null; public sealed override string? Icon => "avares://BD.WTTS.Client.Plugins.GameAccount/UI/Assets/userswitcher.ico"; diff --git a/src/BD.WTTS.Client.Plugins.GameList/Plugins/Plugin.cs b/src/BD.WTTS.Client.Plugins.GameList/Plugins/Plugin.cs index 7a34f11b1..3c68cebd4 100644 --- a/src/BD.WTTS.Client.Plugins.GameList/Plugins/Plugin.cs +++ b/src/BD.WTTS.Client.Plugins.GameList/Plugins/Plugin.cs @@ -17,7 +17,7 @@ public sealed class Plugin : PluginBase, IPlugin public sealed override string Description => "管理库存游戏"; - public sealed override string Author => "Steam++ 官方"; + protected sealed override string? AuthorOriginalString => null; public sealed override string? Icon => "avares://BD.WTTS.Client.Plugins.GameList/UI/Assets/game.ico"; diff --git a/src/BD.WTTS.Client.Plugins.GameTools/Plugins/Plugin.cs b/src/BD.WTTS.Client.Plugins.GameTools/Plugins/Plugin.cs index e383b50c2..1e3466b5b 100644 --- a/src/BD.WTTS.Client.Plugins.GameTools/Plugins/Plugin.cs +++ b/src/BD.WTTS.Client.Plugins.GameTools/Plugins/Plugin.cs @@ -15,7 +15,7 @@ public sealed class Plugin : PluginBase, IPlugin public sealed override string Description => "游戏工具"; - public sealed override string Author => "Steam++ 官方"; + protected sealed override string? AuthorOriginalString => null; public sealed override string? Icon => "avares://BD.WTTS.Client.Plugins.GameTools/UI/Assets/toolbox.ico"; diff --git a/src/BD.WTTS.Client.Tools.Publish/BD.WTTS.Client.Tools.Publish.csproj b/src/BD.WTTS.Client.Tools.Publish/BD.WTTS.Client.Tools.Publish.csproj index acade3f44..2d9780a8b 100644 --- a/src/BD.WTTS.Client.Tools.Publish/BD.WTTS.Client.Tools.Publish.csproj +++ b/src/BD.WTTS.Client.Tools.Publish/BD.WTTS.Client.Tools.Publish.csproj @@ -1,4 +1,4 @@ - + pub diff --git a/src/BD.WTTS.Client.Tools.Publish/Commands/IDotNetPublishCommand.cs b/src/BD.WTTS.Client.Tools.Publish/Commands/IDotNetPublishCommand.cs index 200059522..0481aec30 100644 --- a/src/BD.WTTS.Client.Tools.Publish/Commands/IDotNetPublishCommand.cs +++ b/src/BD.WTTS.Client.Tools.Publish/Commands/IDotNetPublishCommand.cs @@ -1,3 +1,4 @@ +using BD.WTTS.Client.Tools.Publish.Helpers; using static BD.WTTS.Client.Tools.Publish.Helpers.DotNetCLIHelper; using static BD.WTTS.GlobalDllImportResolver; @@ -65,7 +66,7 @@ interface IDotNetPublishCommand : ICommand Console.WriteLine(string.Join(' ', psi.ArgumentList)); var publishDir = Path.Combine(projRootPath, arg.PublishDir); - var rootPublishDir = Path.Combine(publishDir, ".."); + var rootPublishDir = Path.GetFullPath(Path.Combine(publishDir, "..")); DirTryDelete(rootPublishDir); // 发布主体 @@ -100,12 +101,29 @@ interface IDotNetPublishCommand : ICommand // 复制运行时 CopyRuntime(rootPublishDir, isWindows, info.Architecture); + if (OperatingSystem.IsWindows() && isWindows) + { + // 打包资源 images + MSIXHelper.MakePri.Start(rootPublishDir); + // 生成 msix 包 + MSIXHelper.MakeAppx.Start(rootPublishDir, GetVersion(), info.Architecture); + // 签名 msix 包 + MSIXHelper.SignTool.Start(rootPublishDir); + } + Console.WriteLine(rootPublishDir); } Console.WriteLine("OK"); } + static string GetVersion() + { + var v = new Version(AssemblyInfo.Version); + static int GetInt32(int value) => value < 0 ? 0 : value; + return $"{GetInt32(v.Major)}.{GetInt32(v.Minor)}.{GetInt32(v.Build)}.{GetInt32(v.Revision)}"; + } + /// /// 将运行时复制到发布根目录下 /// @@ -630,37 +648,4 @@ interface IDotNetPublishCommand : ICommand } } } - - static void CopyDirectory(string sourceDir, string destinationDir, bool recursive) // https://learn.microsoft.com/zh-cn/dotnet/standard/io/how-to-copy-directories - { - // Get information about the source directory - var dir = new DirectoryInfo(sourceDir); - - // Check if the source directory exists - if (!dir.Exists) - throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); - - // Cache directories before we start copying - DirectoryInfo[] dirs = dir.GetDirectories(); - - // Create the destination directory - Directory.CreateDirectory(destinationDir); - - // Get the files in the source directory and copy to the destination directory - foreach (FileInfo file in dir.GetFiles()) - { - string targetFilePath = Path.Combine(destinationDir, file.Name); - file.CopyTo(targetFilePath, true); - } - - // If recursive and copying subdirectories, recursively call this method - if (recursive) - { - foreach (DirectoryInfo subDir in dirs) - { - string newDestinationDir = Path.Combine(destinationDir, subDir.Name); - CopyDirectory(subDir.FullName, newDestinationDir, true); - } - } - } } diff --git a/src/BD.WTTS.Client.Tools.Publish/Commands/IGenerateCaCertCommand.cs b/src/BD.WTTS.Client.Tools.Publish/Commands/IGenerateCaCertCommand.cs new file mode 100644 index 000000000..e08f3114f --- /dev/null +++ b/src/BD.WTTS.Client.Tools.Publish/Commands/IGenerateCaCertCommand.cs @@ -0,0 +1,90 @@ +namespace BD.WTTS.Client.Tools.Publish.Commands; + +interface IGenerateCaCertCommand : ICommand +{ + const string commandName = "gcert"; + const string X500DistinguishedNameValue = $"C=CN, S=Hunan, L=Changsha, O=\u6C5F\u82CF\u84B8\u6C7D\u51E1\u661F\u79D1\u6280\u6709\u9650\u516C\u53F8, OU=\u6280\u672F\u90E8, CN=\u6C5F\u82CF\u84B8\u6C7D\u51E1\u661F\u79D1\u6280\u6709\u9650\u516C\u53F8\u4EE3\u7801\u7B7E\u540D\u8BC1\u4E66"; + + public const int CertificateValidDays = 3650; + + public const int KEY_SIZE_BITS = 4096; + + static Command ICommand.GetCommand() + { + var type = new Option("--type", + "Generate certificate type(1 code, 2 store-code)"); + var path = new Option("--path", + "Directory where certificate files need to be saved"); + var cn = new Option("--cn", "X500DistinguishedNameValue"); + var password = new Option("--pwd"); + var command = new Command(commandName, "Generate certificate") + { + type, path, cn, password, + }; + command.SetHandler(Handler, type, path, cn, password); + return command; + } + + // New-SelfSignedCertificate -Type Custom -Subject "CN=Contoso Software, O=Contoso Corporation, C=US" -KeyUsage DigitalSignature -FriendlyName "Your friendly name goes here" -CertStoreLocation "Cert:\CurrentUser\My" -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}") + + /// + /// 表示证书对代码签名有效。始终指定此值以限制证书的预期用途。 + /// https://oidref.com/1.3.6.1.5.5.7.3.3 + /// + const string codeSigningOid = "1.3.6.1.5.5.7.3.3"; + + /// + /// 表示证书遵循生存期签名。通常,如果签名带有时间戳,只要证书在时间戳时有效,即使证书过期,签名也仍然有效。此 EKU 强制签名过期,无论签名是否带有时间戳。 + /// https://oidref.com/1.3.6.1.4.1.311.10.3.13 + /// + const string _13Oid = "1.3.6.1.4.1.311.10.3.13"; + + static byte[] GenerateCodeSigningCert(string x500DistinguishedNameValue, string password) + { + if (string.IsNullOrWhiteSpace(password)) + throw new ArgumentOutOfRangeException(nameof(password), password, null); + + // https://learn.microsoft.com/zh-cn/windows/win32/appxpkg/how-to-create-a-package-signing-certificate + + using var rsa = RSA.Create(KEY_SIZE_BITS); + X500DistinguishedName subjectName = new(x500DistinguishedNameValue); + CertificateRequest request = new(subjectName, rsa, + HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1); + + // 基本约束:此扩展指示证书是否为证书颁发机构 (CA) 。 对于自签名证书, + // 此参数应包含扩展字符串 “2.5.29.19={text}”,指示证书是最终实体 (不是 CA) 。 + X509BasicConstraintsExtension basicConstraints = new(false, false, 0, true); + request.CertificateExtensions.Add(basicConstraints); + + X509KeyUsageExtension keyUsage = new(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyCertSign, true); + request.CertificateExtensions.Add(keyUsage); + + var oids = new OidCollection { new(codeSigningOid), new(_13Oid), }; + var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(oids, true); + request.CertificateExtensions.Add(enhancedKeyUsage); + + X509SubjectKeyIdentifierExtension subjectKeyId; + + subjectKeyId = new(request.PublicKey, false); + request.CertificateExtensions.Add(subjectKeyId); + + var notBefore = DateTimeOffset.UtcNow; + var notAfter = notBefore.AddDays(CertificateValidDays); + using var cert = request.CreateSelfSigned(notBefore, notAfter); + var pfx = cert.Export(X509ContentType.Pfx, password); + return pfx; + } + + internal static void Handler(int type, string path, string cn, string password) + { + var bytes = type switch + { + // 生成 BeyondDimension 代码签名证书 + 1 => GenerateCodeSigningCert(X500DistinguishedNameValue, password), + // 生成 上传应用商店的 代码签名证书 + 2 => GenerateCodeSigningCert($"CN={cn}", password), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), + }; + File.WriteAllBytes(path, bytes); + } +} diff --git a/src/BD.WTTS.Client.Tools.Publish/Constants.cs b/src/BD.WTTS.Client.Tools.Publish/Constants.cs index 7de48394f..88519d24f 100644 --- a/src/BD.WTTS.Client.Tools.Publish/Constants.cs +++ b/src/BD.WTTS.Client.Tools.Publish/Constants.cs @@ -181,4 +181,37 @@ interface Constants var download_link = $"{(no_cdn.Value ? feed_no_cdn : feed)}/aspnetcore/Runtime/{specific_version}/aspnetcore-runtime-{specific_version}-{osname}-{normalized_architecture}.{(osname == "win" ? "zip" : "tar.gz")}"; return download_link; } + + static void CopyDirectory(string sourceDir, string destinationDir, bool recursive) // https://learn.microsoft.com/zh-cn/dotnet/standard/io/how-to-copy-directories + { + // Get information about the source directory + var dir = new DirectoryInfo(sourceDir); + + // Check if the source directory exists + if (!dir.Exists) + throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); + + // Cache directories before we start copying + DirectoryInfo[] dirs = dir.GetDirectories(); + + // Create the destination directory + IOPath.DirCreateByNotExists(destinationDir); + + // Get the files in the source directory and copy to the destination directory + foreach (FileInfo file in dir.GetFiles()) + { + string targetFilePath = Path.Combine(destinationDir, file.Name); + file.CopyTo(targetFilePath, true); + } + + // If recursive and copying subdirectories, recursively call this method + if (recursive) + { + foreach (DirectoryInfo subDir in dirs) + { + string newDestinationDir = Path.Combine(destinationDir, subDir.Name); + CopyDirectory(subDir.FullName, newDestinationDir, true); + } + } + } } diff --git a/src/BD.WTTS.Client.Tools.Publish/Helpers/MSIXHelper.cs b/src/BD.WTTS.Client.Tools.Publish/Helpers/MSIXHelper.cs index bb24295bb..42e20fed8 100644 --- a/src/BD.WTTS.Client.Tools.Publish/Helpers/MSIXHelper.cs +++ b/src/BD.WTTS.Client.Tools.Publish/Helpers/MSIXHelper.cs @@ -2,84 +2,6 @@ namespace BD.WTTS.Client.Tools.Publish.Helpers; static class MSIXHelper { - static void W(Stream stream, string version) - { - // https://learn.microsoft.com/zh-cn/windows/msix/desktop/desktop-to-uwp-manual-conversion - var appxmanifest = -""" - - - - - Watt Toolkit - 软妹币玩家 - Images\StoreLogo.png - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"""u8; - } - enum WinVersion : ushort { W10_20H1 = 19041, @@ -87,7 +9,69 @@ static class MSIXHelper W11_22H2 = 22621, } - static class MakeAppx + public static class MakePri + { + const string makepri_exe = "makepri.exe"; + + static string GetMakePriPath(WinVersion version) + { + var path = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + path = Path.Combine(path, "Windows Kits", "10", "bin", $"10.0.{(ushort)version}.0", RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(), makepri_exe); + return path; + } + + static string GetMakePriPath() + { + var query = from m in Enum.GetValues().OrderByDescending(x => (ushort)x) + let p = GetMakePriPath(m) + let exists = File.Exists(p) + where exists + select p; + var path = query.FirstOrDefault(); + if (string.IsNullOrWhiteSpace(path)) + throw new FileNotFoundException(makepri_exe); + return path; + } + + public static void Start(string rootPublicPath) + { + // https://learn.microsoft.com/en-us/windows/uwp/app-resources/makepri-exe-command-options + + var xmlPath = @$"{rootPublicPath}\priconfig.xml"; + + IOPath.FileIfExistsItDelete(xmlPath); + + var fileName = GetMakePriPath(); + var psi = new ProcessStartInfo + { + FileName = fileName, + UseShellExecute = false, + Arguments = +$""" +createconfig /cf "{xmlPath}" /dq lang-en-US /o /pv 10.0.0 +""", + WorkingDirectory = rootPublicPath, + }; + DotNetCLIHelper.StartProcessAndWaitForExit(psi); + var prPath = $@"{ProjectUtils.ProjPath}\build\windows\makepri"; + CopyDirectory(prPath, rootPublicPath, true); + psi = new ProcessStartInfo + { + FileName = fileName, + UseShellExecute = false, + Arguments = +$""" +new /cf "{xmlPath}" /pr "{prPath}" +""", + WorkingDirectory = rootPublicPath, + }; + DotNetCLIHelper.StartProcessAndWaitForExit(psi); + + IOPath.FileIfExistsItDelete(xmlPath); + } + } + + public static class MakeAppx { // https://learn.microsoft.com/zh-cn/windows/msix/package/create-app-package-with-makeappx-tool // 使用 MakeAppx.exe 创建 MSIX 包或捆绑包 @@ -114,13 +98,176 @@ static class MSIXHelper return path; } - public static void Start(string arguments) + public static void Start( + string rootPublicPath, + string version4, + Architecture processorArchitecture) { + GenerateAppxManifestXml(rootPublicPath, version4, processorArchitecture); + + var msixPath = $"{rootPublicPath}.msix"; + IOPath.FileIfExistsItDelete(msixPath); + var psi = new ProcessStartInfo { FileName = GetMakeAppxPath(), UseShellExecute = false, - Arguments = arguments, + Arguments = +$""" +pack /v /h SHA256 /d "{rootPublicPath}" /p "{msixPath}" +""", + }; + DotNetCLIHelper.StartProcessAndWaitForExit(psi); + } + + /// + /// 生成位于根目录的 AppxManifest.xml + /// + /// + /// + /// + static void GenerateAppxManifestXml( + string rootPublicPath, + string version4, + Architecture processorArchitecture) + { + // https://learn.microsoft.com/zh-cn/windows/msix/desktop/desktop-to-uwp-manual-conversion + + var xmlString = +$""" + + + + + Watt Toolkit + 软妹币玩家 + images\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"""; + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(xmlString); + var xmlStringMini = xmlDoc.InnerXml; + var xmlFilePath = Path.Combine(rootPublicPath, "AppxManifest.xml"); + File.WriteAllText(xmlFilePath, xmlStringMini); + } + + // /// + // /// 生成打包布局 + // /// + // static void GeneratePackagingLayoutXml( + // string rootPublicPath, + // string appxManifestXmlPath, + // Architecture processorArchitecture) + // { + // // 使用打包布局创建包 + // // https://learn.microsoft.com/zh-cn/windows/msix/package/packaging-layout + + // var xmlString = + //""" + // + // + // + // + // + // + // + // + // + // + //"""u8; + // Stream stream = new MemoryStream(); + // stream.WriteFormat(xmlString, new object?[] + // { + // appxManifestXmlPath, + // processorArchitecture.ToString().ToLowerInvariant(), + // rootPublicPath, + // }); + // } + } + + public static class SignTool + { + const string SignTool_exe = "SignTool.exe"; + + static string GetSignToolPath(WinVersion version) + { + var path = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + path = Path.Combine(path, "Windows Kits", "10", "bin", $"10.0.{(ushort)version}.0", RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(), SignTool_exe); + return path; + } + + static string GetSignToolPath() + { + var query = from m in Enum.GetValues().OrderByDescending(x => (ushort)x) + let p = GetSignToolPath(m) + let exists = File.Exists(p) + where exists + select p; + var path = query.FirstOrDefault(); + if (string.IsNullOrWhiteSpace(path)) + throw new FileNotFoundException(SignTool_exe); + return path; + } + + public static void Start( + string rootPublicPath) + { + var pwd = File.ReadAllText(@"C:\MSStore_CodeSigning.txt"); + var psi = new ProcessStartInfo + { + FileName = GetSignToolPath(), + UseShellExecute = false, + Arguments = +$""" +sign /a /fd SHA384 /f "C:\MSStore_CodeSigning.pfx" /p {pwd} "{rootPublicPath}.msix" +""", }; DotNetCLIHelper.StartProcessAndWaitForExit(psi); } diff --git a/src/BD.WTTS.Client.Tools.Publish/Program.cs b/src/BD.WTTS.Client.Tools.Publish/Program.cs index deaa21c62..1e4100741 100644 --- a/src/BD.WTTS.Client.Tools.Publish/Program.cs +++ b/src/BD.WTTS.Client.Tools.Publish/Program.cs @@ -1,3 +1,5 @@ +IGenerateCaCertCommand.GenerateCodeSigningCert(IGenerateCaCertCommand.X500DistinguishedNameValue, "aaa"); + var rootCommand = new RootCommand($"{AssemblyInfo.Product} Publish Tools"); var interfaceType = typeof(ICommand); var addMethod = interfaceType.GetMethod(nameof(ICommand.AddCommand), BindingFlags.Static | BindingFlags.Public); diff --git a/src/BD.WTTS.Client/Helpers/ReflectionHelper.cs b/src/BD.WTTS.Client/Helpers/ReflectionHelper.cs new file mode 100644 index 000000000..e966e8de2 --- /dev/null +++ b/src/BD.WTTS.Client/Helpers/ReflectionHelper.cs @@ -0,0 +1,43 @@ +// ReSharper disable once CheckNamespace +namespace BD.WTTS; + +static class ReflectionHelper +{ + /// + /// 是否为内部调用,防止动态反射调用 + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInternalCall(Assembly thisAssembly, StackTrace stackTrace) + { + if (stackTrace.FrameCount >= 1) + { + for (int i = 1; i < stackTrace.FrameCount; i++) + { + try + { + var method = stackTrace.GetFrame(i)?.GetMethod(); + var assembly = method?.Module.Assembly; + if (assembly == thisAssembly) + { + return true; + } + } + catch + { + + } + if (i == 2) + break; + } + } + return false; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInternalCall(StackTrace stackTrace) + => IsInternalCall(typeof(T).Assembly, stackTrace); +} diff --git a/src/BD.WTTS.Client/Plugins/Abstractions/IPlugin.Properties.cs b/src/BD.WTTS.Client/Plugins/Abstractions/IPlugin.Properties.cs index 48740c847..9014e66a3 100644 --- a/src/BD.WTTS.Client/Plugins/Abstractions/IPlugin.Properties.cs +++ b/src/BD.WTTS.Client/Plugins/Abstractions/IPlugin.Properties.cs @@ -81,4 +81,9 @@ partial interface IPlugin /// 插件安装时间 /// DateTimeOffset InstallTime { get; } + + /// + /// 插件发布时间 + /// + DateTimeOffset ReleaseTime { get; } } diff --git a/src/BD.WTTS.Client/Plugins/Abstractions/PluginBase.Properties.cs b/src/BD.WTTS.Client/Plugins/Abstractions/PluginBase.Properties.cs index dfe2cf941..66cc9ace7 100644 --- a/src/BD.WTTS.Client/Plugins/Abstractions/PluginBase.Properties.cs +++ b/src/BD.WTTS.Client/Plugins/Abstractions/PluginBase.Properties.cs @@ -23,7 +23,51 @@ partial class PluginBase public virtual Type? SettingsPageViewType { get; } - public virtual string? Author { get; } + const string OfficialAuthor = "Steam++ 官方"; + const string IllegalAuthor = "存在非法字符"; + const string UnknownAuthor = "未知"; + + /// + /// 作者名字符串中是否存在非法字符 + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool IsIllegalAuthor(string author) + { + if (author.Contains("Steam++", StringComparison.OrdinalIgnoreCase) || + author.Contains("Steam", StringComparison.OrdinalIgnoreCase) || + author.Contains("steampp", StringComparison.OrdinalIgnoreCase) || + author.Contains("Watt", StringComparison.OrdinalIgnoreCase) || + author.Contains("WattToolkit", StringComparison.OrdinalIgnoreCase) || + author.Contains("\u6B21\u5143\u8D85\u8D8A", StringComparison.OrdinalIgnoreCase) || + author.Contains("\u51E1\u661F", StringComparison.OrdinalIgnoreCase) || + author.Contains("\u7E41\u661F", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return false; + } + + public string? Author + { + get + { + if (IsOfficial) + return OfficialAuthor; + var author = AuthorOriginalString; + if (string.IsNullOrWhiteSpace(author)) + return UnknownAuthor; + if (IsIllegalAuthor(author)) + return IllegalAuthor; + return author; + } + } + + /// + /// 插件作者名(由插件程序集重写此属性填写) + /// + protected virtual string? AuthorOriginalString { get; } public virtual string AuthorStoreUrl => $"https://steampp.net/store/plugins/authors/{WebUtility.UrlEncode(Author)}"; @@ -36,63 +80,109 @@ partial class PluginBase public string CacheDirectory => mCacheDirectory.Value; - //static byte[][] OfficialPluginHashDatas => Array.Empty(); + ///// + ///// 官方插件的哈希值 SHA256 数据,可使用 API 调用服务端获取 + ///// + //internal static SecurityCriticalOfficialPluginHashDatas OfficialPluginHashDatas { private get; set; } - bool? mIsOfficial; + //internal readonly record struct SecurityCriticalOfficialPluginHashDatas + //{ + // internal SecurityCriticalOfficialPluginHashDatas(ImmutableArray value) + // { + // StackTrace stackTrace = new(); + // var isInternalCall = ReflectionHelper.IsInternalCall(stackTrace); + // if (!isInternalCall) + // throw new ApplicationException("Disable reflection calls to this constructor."); + + // Value = value; + // } + + // internal ImmutableArray Value { get; init; } + + // [MethodImpl(MethodImplOptions.AggressiveInlining)] + // internal bool IsDefault() => this == default || Value == default || Value.IsEmpty; + //} + + /// + /// 是否为嵌入式插件,嵌入在程序安装目录中的 + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + bool IsEmbeddedPlugin() + { + try + { + var rootPath = Path.GetFullPath(Path.Combine(AssemblyLocation, +#if DEBUG + "..", "..", "..", "..", ".." +#else + "..", ".." +#endif + )); + var value = Environment.ProcessPath!.StartsWith(rootPath); + return value; + } + catch + { + } + return false; + } public bool IsOfficial { - get + get // 实时计算,不缓存结果 { - //#if DEBUG - //switch (Name) - //{ - // case AssemblyInfo.Accelerator: - // case AssemblyInfo.ArchiSteamFarmPlus: - // case AssemblyInfo.Authenticator: - // case AssemblyInfo.GameAccount: - // case AssemblyInfo.GameList: - // case AssemblyInfo.GameTools: - // return true; - //} - //#endif - if (mIsOfficial.HasValue) - return mIsOfficial.Value; - //try - //{ - // byte[] hashData; - // using (var fileStream = new FileStream(AssemblyLocation, - // FileMode.Open, - // FileAccess.Read, - // FileShare.ReadWrite | FileShare.Delete)) - // { - // hashData = SHA256.HashData(fileStream); - // } - // if (PluginBase.OfficialPluginHashDatas.Contains(hashData)) - // { - // mIsOfficial = true; - // return true; - // } - //} - //catch - //{ + // if (!OfficialPluginHashDatas.IsDefault()) + // { + // try + // { + // byte[] hashData; + // using (var fileStream = new FileStream(AssemblyLocation, + // FileMode.Open, + // FileAccess.Read, + // FileShare.ReadWrite | FileShare.Delete)) + // { + // hashData = SHA256.HashData(fileStream); + // } + // if (OfficialPluginHashDatas.Value.Contains(hashData)) + // { + // return true; + // } + // } + // catch + // { - //} + // } + // } + //#if DEBUG var thisType = GetType(); - mIsOfficial = thisType.Name == "BD.WTTS.Plugins.Plugin" && + var value = thisType.FullName == "BD.WTTS.Plugins.Plugin" && thisType.Assembly.GetName().Name?.TrimStart("BD.WTTS.Client.Plugins.") switch { - AssemblyInfo.Accelerator or - AssemblyInfo.ArchiSteamFarmPlus or - AssemblyInfo.Authenticator or - AssemblyInfo.GameAccount or - AssemblyInfo.GameList or - AssemblyInfo.GameTools => true, + AssemblyInfo.Accelerator => IsEmbeddedPlugin() && + UniqueEnglishName == AssemblyInfo.Accelerator && + Id.ToString() == AssemblyInfo.AcceleratorId, + AssemblyInfo.ArchiSteamFarmPlus => IsEmbeddedPlugin() && + UniqueEnglishName == AssemblyInfo.ArchiSteamFarmPlus && + Id.ToString() == AssemblyInfo.ArchiSteamFarmPlusId, + AssemblyInfo.Authenticator => IsEmbeddedPlugin() && + UniqueEnglishName == AssemblyInfo.Authenticator && + Id.ToString() == AssemblyInfo.AuthenticatorId, + AssemblyInfo.GameAccount => IsEmbeddedPlugin() && + UniqueEnglishName == AssemblyInfo.GameAccount && + Id.ToString() == AssemblyInfo.GameAccountId, + AssemblyInfo.GameList => IsEmbeddedPlugin() && + UniqueEnglishName == AssemblyInfo.GameList && + Id.ToString() == AssemblyInfo.GameListId, + AssemblyInfo.GameTools => IsEmbeddedPlugin() && + UniqueEnglishName == AssemblyInfo.GameTools && + Id.ToString() == AssemblyInfo.GameToolsId, _ => false, }; - return mIsOfficial.Value; - //mIsOfficial = false; - //return false; + return value; + //#else + // return false; + //#endif } } @@ -114,6 +204,8 @@ partial class PluginBase } public DateTimeOffset InstallTime => mInstallTime.Value; + + public DateTimeOffset ReleaseTime { get; internal set; } } partial class PluginBase diff --git a/src/BD.WTTS.Client/Plugins/PluginsCore.cs b/src/BD.WTTS.Client/Plugins/PluginsCore.cs index 4f0d96fb0..f2b07c886 100644 --- a/src/BD.WTTS.Client/Plugins/PluginsCore.cs +++ b/src/BD.WTTS.Client/Plugins/PluginsCore.cs @@ -21,42 +21,16 @@ public static class PluginsCore static DisablePluginsAssemblyLoadContext disablePluginsAssemblyLoadContext = new(); - sealed class DisablePlugin : IPlugin + [DebuggerDisplay("{DebuggerDisplay,nq}")] + sealed record class DisablePlugin : IPlugin { - public Guid Id { get; private set; } - - public string Name { get; private set; } = null!; - - public string UniqueEnglishName { get; private set; } = null!; - - public string Version { get; private set; } = null!; - - public string? Description { get; private set; } - - public string StoreUrl { get; private set; } = null!; - - public string HelpUrl { get; private set; } = null!; - - public Type? SettingsPageViewType => null; - - public string? Author { get; private set; } - - public string AuthorStoreUrl { get; private set; } = null!; - - public string AssemblyLocation { get; private set; } = null!; - - public string AppDataDirectory { get; private set; } = null!; - - public string CacheDirectory { get; private set; } = null!; - - public bool IsOfficial { get; private set; } - - public string? Icon { get; private set; } - - public DateTimeOffset InstallTime { get; private set; } - - public void SetValue(IPlugin plugin) + public DisablePlugin(IPlugin plugin) { + StackTrace stackTrace = new(); + var isInternalCall = ReflectionHelper.IsInternalCall(stackTrace); + if (!isInternalCall) + throw new ApplicationException("Disable reflection calls to this constructor."); + Id = plugin.Id; Name = plugin.Name; UniqueEnglishName = plugin.UniqueEnglishName; @@ -72,8 +46,47 @@ public static class PluginsCore IsOfficial = plugin.IsOfficial; Icon = plugin.Icon; InstallTime = plugin.InstallTime; + ReleaseTime = plugin.ReleaseTime; } + string DebuggerDisplay => $"{UniqueEnglishName} v{Version}"; + + public override string ToString() => DebuggerDisplay; + + public Guid Id { get; init; } + + public string Name { get; init; } + + public string UniqueEnglishName { get; init; } + + public string Version { get; init; } + + public string? Description { get; init; } + + public string StoreUrl { get; init; } + + public string HelpUrl { get; init; } + + public Type? SettingsPageViewType => null; + + public string? Author { get; init; } + + public string AuthorStoreUrl { get; init; } + + public string AssemblyLocation { get; init; } + + public string AppDataDirectory { get; init; } + + public string CacheDirectory { get; init; } + + public bool IsOfficial { get; init; } + + public string? Icon { get; init; } + + public DateTimeOffset InstallTime { get; init; } + + public DateTimeOffset ReleaseTime { get; init; } + public void ConfigureDemandServices(IServiceCollection services, Startup startup) { } @@ -309,9 +322,12 @@ public static class PluginsCore using CompositionHost container = configuration.CreateContainer(); foreach (var plugin in container.GetExports()) { - if (plugin.HasValue() && plugin is PluginBase) + if (plugin.HasValue() && plugin is PluginBase pluginBase) { - plugins.Add(plugin); +#if DEBUG + var isOfficial = pluginBase.IsOfficial; +#endif + plugins.Add(pluginBase); } else { @@ -408,9 +424,15 @@ public static class PluginsCore { foreach (var plugin in disablePlugins) { - var plugin_ = new DisablePlugin(); - plugin_.SetValue(plugin); - pluginResults.Add(new(true, plugin_)); + try + { + var plugin_ = new DisablePlugin(plugin); + pluginResults.Add(new(true, plugin_)); + } + catch (Exception e) + { + Log.Error(TAG, e, "Failed to initialize disabled plugin."); + } } disablePluginsAssemblyLoadContext.Unload(); disablePluginsAssemblyLoadContext = null!; diff --git a/src/BD.WTTS.Client/Startup/Startup.cs b/src/BD.WTTS.Client/Startup/Startup.cs index e330d12e8..ad5f91c20 100644 --- a/src/BD.WTTS.Client/Startup/Startup.cs +++ b/src/BD.WTTS.Client/Startup/Startup.cs @@ -115,6 +115,7 @@ public abstract partial class Startup throw new ArgumentOutOfRangeException("CurrentThread != ApartmentState.STA"); } } + #endif try