diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs new file mode 100644 index 0000000..83de71a --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs @@ -0,0 +1,368 @@ +// AssemblyLoader.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace WelsonJS.Launcher +{ + /// + /// Network-aware loader for managed (.NET) and native (C/C++) binaries. + /// - Managed assemblies resolve via AssemblyResolve + /// - Native modules explicitly loaded via LoadNativeModules(...) + /// - All DLLs must have valid Authenticode signatures + /// - Cached at: %APPDATA%\WelsonJS\assembly\{Name}\{Version}\ + /// - BaseUrl must be set by Main() before calling Register() + /// + public static class AssemblyLoader + { + /// + /// Base URL for downloading managed/native binaries. + /// Example: https://catswords.blob.core.windows.net/welsonjs/packages + /// Must be set before Register() or LoadNativeModules(). + /// + public static string BaseUrl { get; set; } = null; + + private static readonly object SyncRoot = new object(); + private static bool _registered; + + private static readonly string LoaderNamespace = typeof(AssemblyLoader).Namespace ?? "WelsonJS.Launcher"; + private static readonly HttpClient Http = new HttpClient(); + private static readonly ICompatibleLogger Logger = new TraceLogger(); + + // -------------------- kernel32 native loading -------------------- + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr LoadLibrary(string lpFileName); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool SetDllDirectory(string lpPathName); + + // -------------------- WinVerifyTrust (signature verification) -------------------- + + private const uint ERROR_SUCCESS = 0x00000000; + private const uint TRUST_E_NOSIGNATURE = 0x800B0100; + private const uint TRUST_E_EXPLICIT_DISTRUST = 0x800B0111; + private const uint TRUST_E_SUBJECT_NOT_TRUSTED = 0x800B0004; + private const uint CRYPT_E_SECURITY_SETTINGS = 0x80092026; + + private static readonly Guid WINTRUST_ACTION = + new Guid("00aac56b-cd44-11d0-8cc2-00c04fc295ee"); + + [DllImport("wintrust.dll", CharSet = CharSet.Unicode)] + private static extern uint WinVerifyTrust( + IntPtr hwnd, + [MarshalAs(UnmanagedType.LPStruct)] Guid pgActionID, + ref WINTRUST_DATA pWVTData); + + + private enum FileSignatureStatus { Valid, NoSignature, Invalid } + + + // -------------------- WinTrust Structures -------------------- + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct WINTRUST_FILE_INFO + { + public uint cbStruct; + [MarshalAs(UnmanagedType.LPWStr)] public string pcwszFilePath; + public IntPtr hFile; + public IntPtr pgKnownSubject; + + public WINTRUST_FILE_INFO(string filePath) + { + cbStruct = (uint)Marshal.SizeOf(typeof(WINTRUST_FILE_INFO)); + pcwszFilePath = filePath; + hFile = IntPtr.Zero; + pgKnownSubject = IntPtr.Zero; + } + } + + private enum WinTrustDataUIChoice : uint { None = 2 } + private enum WinTrustDataRevocationChecks : uint { None = 0 } + private enum WinTrustDataChoice : uint { File = 1 } + private enum WinTrustDataStateAction : uint { Ignore = 0 } + private enum WinTrustDataUIContext : uint { Execute = 0 } + [Flags] + private enum WinTrustDataProvFlags : uint + { + RevocationCheckNone = 0x00000010, + DisableMD2andMD4 = 0x00002000 + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct WINTRUST_DATA + { + public uint cbStruct; + public IntPtr pPolicyCallbackData; + public IntPtr pSIPClientData; + public WinTrustDataUIChoice dwUIChoice; + public WinTrustDataRevocationChecks dwRevocationChecks; + public WinTrustDataChoice dwUnionChoice; + public IntPtr pFile; + public WinTrustDataStateAction dwStateAction; + public IntPtr hWVTStateData; + public string pwszURLReference; + public WinTrustDataProvFlags dwProvFlags; + public WinTrustDataUIContext dwUIContext; + + public WINTRUST_DATA(IntPtr pFileInfo) + { + cbStruct = (uint)Marshal.SizeOf(typeof(WINTRUST_DATA)); + pPolicyCallbackData = IntPtr.Zero; + pSIPClientData = IntPtr.Zero; + dwUIChoice = WinTrustDataUIChoice.None; + dwRevocationChecks = WinTrustDataRevocationChecks.None; + dwUnionChoice = WinTrustDataChoice.File; + pFile = pFileInfo; + dwStateAction = WinTrustDataStateAction.Ignore; + hWVTStateData = IntPtr.Zero; + pwszURLReference = null; + dwProvFlags = WinTrustDataProvFlags.RevocationCheckNone | + WinTrustDataProvFlags.DisableMD2andMD4; + dwUIContext = WinTrustDataUIContext.Execute; + } + } + + + // ======================================================================== + // PUBLIC API + // ======================================================================== + + /// + /// Registers AssemblyResolve to download and validate .NET assemblies. + /// + public static void Register() + { + if (_registered) + return; + + if (string.IsNullOrWhiteSpace(BaseUrl)) + { + Logger.Error("AssemblyLoader.Register() called but BaseUrl is not set."); + throw new InvalidOperationException("AssemblyLoader.BaseUrl must be configured before Register()."); + } + + AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve; + _registered = true; + + Logger.Info("AssemblyLoader: AssemblyResolve handler registered."); + } + + + /// + /// Loads native modules associated with an assembly (explicit). + /// + public static void LoadNativeModules(string ownerAssemblyName, Version version, IList fileNames) + { + if (string.IsNullOrWhiteSpace(BaseUrl)) + throw new InvalidOperationException("AssemblyLoader.BaseUrl must be set before loading native modules."); + + if (ownerAssemblyName == null) throw new ArgumentNullException("ownerAssemblyName"); + if (version == null) throw new ArgumentNullException("version"); + if (fileNames == null) throw new ArgumentNullException("fileNames"); + + string versionString = version.ToString(); + + lock (SyncRoot) + { + string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string cacheDir = Path.Combine(appData, "WelsonJS", "assembly", ownerAssemblyName, versionString); + Directory.CreateDirectory(cacheDir); + + try { SetDllDirectory(cacheDir); } + catch { } + + foreach (string raw in fileNames) + { + if (string.IsNullOrWhiteSpace(raw)) + continue; + + string fileName = raw.Trim(); + string localPath = Path.Combine(cacheDir, fileName); + + if (!File.Exists(localPath)) + { + string url = $"{BaseUrl.TrimEnd('/')}/native/{ownerAssemblyName}/{versionString}/{fileName}"; + DownloadFile(url, localPath); + Logger.Info("Downloaded native module: {0}", fileName); + } + else + { + Logger.Info("Using cached native module: {0}", localPath); + } + + EnsureSignedFileOrThrow(localPath, fileName); + + IntPtr h = LoadLibrary(localPath); + if (h == IntPtr.Zero) + { + Logger.Error("LoadLibrary failed for {0}", localPath); + } + else + { + Logger.Info("Loaded native module: {0}", fileName); + } + } + } + } + + + public static void LoadNativeModules(Assembly asm, IList fileNames) + { + AssemblyName an = asm.GetName(); + LoadNativeModules(an.Name, an.Version, fileNames); + } + + + // ======================================================================== + // ASSEMBLY RESOLVE HANDLER (MANAGED) + // ======================================================================== + + private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args) + { + Logger.Info("AssemblyResolve: {0}", args.Name); + + AssemblyName req = new AssemblyName(args.Name); + string simpleName = req.Name; + if (IsFrameworkAssembly(simpleName)) + return null; + + var entry = Assembly.GetEntryAssembly(); + if (entry != null) + { + var entryName = entry.GetName().Name; + if (string.Equals(simpleName, entryName, StringComparison.OrdinalIgnoreCase)) + { + Logger.Info("AssemblyResolve: skipping entry assembly {0}", simpleName); + return null; + } + } + + Version version = req.Version ?? new Version(0, 0, 0, 0); + string versionStr = version.ToString(); + + lock (SyncRoot) + { + string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string cacheDir = Path.Combine(appData, "WelsonJS", "assembly", simpleName, versionStr); + string dllPath = Path.Combine(cacheDir, simpleName + ".dll"); + + Directory.CreateDirectory(cacheDir); + + if (!File.Exists(dllPath)) + { + string url = $"{BaseUrl.TrimEnd('/')}/managed/{simpleName}/{versionStr}/{simpleName}.dll"; + DownloadFile(url, dllPath); + Logger.Info("Downloaded managed assembly: {0}", simpleName); + } + else + { + Logger.Info("Using cached managed assembly: {0}", dllPath); + } + + if (!File.Exists(dllPath)) + { + Logger.Warn("AssemblyResolve: managed assembly not found after download attempt: {0}", simpleName); + return null; + } + + EnsureSignedFileOrThrow(dllPath, simpleName); + return Assembly.LoadFrom(dllPath); + } + } + + + // ======================================================================== + // HELPERS + // ======================================================================== + + private static void DownloadFile(string url, string dest) + { + HttpResponseMessage res = null; + + try + { + res = Http.GetAsync(url).GetAwaiter().GetResult(); + if (res.StatusCode == HttpStatusCode.NotFound) + { + Logger.Warn("DownloadFile: 404 Not Found for {0}", url); + return; + } + + res.EnsureSuccessStatusCode(); + + using (Stream s = res.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) + using (FileStream fs = new FileStream(dest, FileMode.Create, FileAccess.Write)) + { + s.CopyTo(fs); + } + } + catch (HttpRequestException ex) + { + Logger.Error("DownloadFile: HTTP error for {0}: {1}", url, ex.Message); + throw; + } + } + + + private static bool IsFrameworkAssembly(string name) + { + return name.StartsWith("System", StringComparison.OrdinalIgnoreCase) || + name.StartsWith("Microsoft", StringComparison.OrdinalIgnoreCase) || + name == "mscorlib" || + name == "netstandard" || + name == "WindowsBase" || + name == "PresentationCore" || + name == "PresentationFramework" || + name.StartsWith(LoaderNamespace); + } + + + private static void EnsureSignedFileOrThrow(string path, string logicalName) + { + FileSignatureStatus status = VerifySignature(path); + + if (status == FileSignatureStatus.Valid) + { + Logger.Info("Signature OK: {0}", logicalName); + return; + } + + if (status == FileSignatureStatus.NoSignature) + { + Logger.Error("BLOCKED unsigned binary: {0}", logicalName); + throw new InvalidOperationException("Unsigned binary blocked: " + logicalName); + } + + Logger.Error("BLOCKED invalid signature: {0}", logicalName); + throw new InvalidOperationException("Invalid signature: " + logicalName); + } + + + private static FileSignatureStatus VerifySignature(string file) + { + WINTRUST_FILE_INFO fileInfo = new WINTRUST_FILE_INFO(file); + IntPtr pFile = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(WINTRUST_FILE_INFO))); + + Marshal.StructureToPtr(fileInfo, pFile, false); + WINTRUST_DATA data = new WINTRUST_DATA(pFile); + + uint result = WinVerifyTrust(IntPtr.Zero, WINTRUST_ACTION, ref data); + + Marshal.FreeCoTaskMem(pFile); + + if (result == ERROR_SUCCESS) return FileSignatureStatus.Valid; + if (result == TRUST_E_NOSIGNATURE) return FileSignatureStatus.NoSignature; + + return FileSignatureStatus.Invalid; + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs deleted file mode 100644 index 609169b..0000000 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs +++ /dev/null @@ -1,244 +0,0 @@ -// NativeBootstrap.cs -// SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors -// https://github.com/gnh1201/welsonjs -// -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Security.Cryptography.X509Certificates; - -namespace WelsonJS.Launcher -{ - public static class NativeBootstrap - { - // Win32 APIs - [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern IntPtr LoadLibrary(string lpFileName); - - [DllImport("kernel32", SetLastError = true)] - private static extern bool SetDefaultDllDirectories(int DirectoryFlags); - - [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "AddDllDirectory")] - private static extern IntPtr AddDllDirectory(string newDirectory); - - [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool SetDllDirectory(string lpPathName); - - private const int LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000; - - /// - /// Tries to load native libraries in the following order: - /// 1) %APPDATA%\{appDataSubdirectory}\{dllName} - /// 2) Application base directory\{dllName} - /// - /// Signatures: - /// - requireSigned = true : Only loads DLLs with a valid Authenticode chain. - /// - certValidator != null : Additional custom validation (e.g., pinning). - /// - /// Must be called before any P/Invoke usage. - /// - public static void Init( - IEnumerable dllNames, - string appDataSubdirectory, - ICompatibleLogger logger, - bool requireSigned = false, - Func certValidator = null) - { - if (dllNames == null) throw new ArgumentNullException(nameof(dllNames)); - if (logger == null) throw new ArgumentNullException(nameof(logger)); - - string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - string appDataPath = string.IsNullOrEmpty(appDataSubdirectory) - ? appData - : Path.Combine(appData, appDataSubdirectory); - - string asmLocation = Assembly.GetEntryAssembly()?.Location - ?? Assembly.GetExecutingAssembly().Location - ?? AppContext.BaseDirectory; - string appBaseDirectory = Path.GetDirectoryName(asmLocation) ?? AppContext.BaseDirectory; - - var triedPaths = new List(); - - foreach (string dllName in dllNames) - { - if (string.IsNullOrWhiteSpace(dllName)) - continue; - - // 1) %APPDATA% subdirectory - string candidate1 = Path.Combine(appDataPath, dllName); - triedPaths.Add(candidate1); - if (TryLoad(candidate1, logger, requireSigned, certValidator)) return; - - // 2) Application base directory - string candidate2 = Path.Combine(appBaseDirectory, dllName); - triedPaths.Add(candidate2); - if (TryLoad(candidate2, logger, requireSigned, certValidator)) return; - } - - string message = "Failed to load requested native libraries.\n" + - "Tried:\n " + string.Join("\n ", triedPaths); - logger.Error(message); - throw new FileNotFoundException(message); - } - - private static bool TryLoad( - string fullPath, - ICompatibleLogger logger, - bool requireSigned, - Func certValidator) - { - try - { - if (!File.Exists(fullPath)) - { - logger.Info($"Not found: {fullPath}"); - return false; - } - - // Optional signature validation - if (!ValidateSignatureIfRequired(fullPath, requireSigned, certValidator, logger)) - { - // If requireSigned=false we never reach here (it would return true). - logger.Warn($"Signature validation failed: {fullPath}"); - return false; - } - - string directoryPath = Path.GetDirectoryName(fullPath) ?? AppContext.BaseDirectory; - if (!TryRegisterSearchDirectory(directoryPath, logger)) - { - logger.Warn($"Could not register search directory: {directoryPath}"); - } - - logger.Info($"Loading: {fullPath}"); - IntPtr handle = LoadLibrary(fullPath); - if (handle == IntPtr.Zero) - { - int err = Marshal.GetLastWin32Error(); - logger.Warn($"LoadLibrary failed for {fullPath} (Win32Error={err})."); - return false; - } - - logger.Info($"Successfully loaded: {fullPath}"); - return true; - } - catch (Exception ex) - { - logger.Warn($"Exception while loading {fullPath}: {ex.Message}"); - return false; - } - } - - /// - /// If requireSigned=false, returns true (no check). - /// If requireSigned=true, verifies Authenticode chain and optional custom validator. - /// - private static bool ValidateSignatureIfRequired( - string path, - bool requireSigned, - Func certValidator, - ICompatibleLogger logger) - { - if (!requireSigned) - { - // No signature requirement: allow loading regardless of signature. - return true; - } - - try - { - // Throws on unsigned files. - var baseCert = X509Certificate.CreateFromSignedFile(path); - if (baseCert == null) - { - logger.Warn("No certificate extracted from file."); - return false; - } - - var cert = new X509Certificate2(baseCert); - - var chain = new X509Chain - { - ChainPolicy = - { - RevocationMode = X509RevocationMode.Online, - RevocationFlag = X509RevocationFlag.ExcludeRoot, - VerificationFlags = X509VerificationFlags.NoFlag, - VerificationTime = DateTime.UtcNow - } - }; - - bool chainOk = chain.Build(cert); - if (!chainOk) - { - foreach (var status in chain.ChainStatus) - logger.Warn($"Cert chain status: {status.Status} - {status.StatusInformation?.Trim()}"); - return false; - } - - // Optional extra validation, e.g. thumbprint or subject pinning. - if (certValidator != null) - { - bool ok = false; - try { ok = certValidator(cert); } - catch (Exception ex) - { - logger.Warn($"Custom certificate validator threw: {ex.Message}"); - return false; - } - - if (!ok) - { - logger.Warn("Custom certificate validator rejected the certificate."); - return false; - } - } - - logger.Info($"Signature validated. Subject='{cert.Subject}', Thumbprint={cert.Thumbprint}"); - return true; - } - catch (Exception ex) - { - logger.Warn($"Signature check failed: {ex.Message}"); - return false; - } - } - - private static bool TryRegisterSearchDirectory(string directoryPath, ICompatibleLogger logger) - { - try - { - bool ok = SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); - if (!ok) - { - int e = Marshal.GetLastWin32Error(); - logger.Warn($"SetDefaultDllDirectories failed (Win32Error={e}), fallback to SetDllDirectory."); - return SetDllDirectory(directoryPath); - } - - IntPtr cookie = AddDllDirectory(directoryPath); - if (cookie == IntPtr.Zero) - { - int e = Marshal.GetLastWin32Error(); - logger.Warn($"AddDllDirectory failed (Win32Error={e}), fallback to SetDllDirectory."); - return SetDllDirectory(directoryPath); - } - - logger.Info($"Registered native DLL search directory: {directoryPath}"); - return true; - } - catch (EntryPointNotFoundException) - { - logger.Warn("DefaultDllDirectories API not available. Using SetDllDirectory fallback."); - return SetDllDirectory(directoryPath); - } - catch (Exception ex) - { - logger.Warn($"Register search directory failed for {directoryPath}: {ex.Message}"); - return false; - } - } - } -} \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index 2ed4b5d..77fc01f 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -32,19 +32,10 @@ namespace WelsonJS.Launcher // set up logger _logger = new TraceLogger(); - // load native libraries - string appDataSubDirectory = "WelsonJS"; - bool requireSigned = string.Equals( - GetAppConfig("NativeRequireSigned"), - "true", - StringComparison.OrdinalIgnoreCase); - - NativeBootstrap.Init( - dllNames: new[] { "ChakraCore.dll" }, - appDataSubdirectory: appDataSubDirectory, - logger: _logger, - requireSigned: requireSigned - ); + // load external assemblies + AssemblyLoader.BaseUrl = GetAppConfig("AssemblyBaseUrl"); + AssemblyLoader.Register(); + AssemblyLoader.LoadNativeModules("ChakraCore", new Version(1, 13, 0, 0), new[] { "ChakraCore.dll" }); // telemetry try diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs index 6ef92bc..0c6cea4 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs @@ -60,6 +60,15 @@ namespace WelsonJS.Launcher.Properties { } } + /// + /// https://catswords.blob.core.windows.net/welsonjs/packages과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string AssemblyBaseUrl { + get { + return ResourceManager.GetString("AssemblyBaseUrl", resourceCulture); + } + } + /// /// 과(와) 유사한 지역화된 문자열을 찾습니다. /// diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx index 0f32638..d0d2661 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx @@ -241,4 +241,7 @@ true + + https://catswords.blob.core.windows.net/welsonjs/packages + \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index a656599..a1a876a 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -88,12 +88,12 @@ + - diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config index 617e7c7..59b2fa6 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config @@ -30,6 +30,7 @@ +