From 07e47338cb1da4b6286e3f39d0286c3ce1c8fb51 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Thu, 4 Dec 2025 14:54:21 +0900 Subject: [PATCH 1/6] Added the assembly loader with Azure Blob Storage Added the assembly loader with Azure Blob Storage --- .../WelsonJS.Launcher/AssemblyLoader.cs | 368 ++++++++++++++++++ .../WelsonJS.Launcher/NativeBootstrap.cs | 244 ------------ WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs | 17 +- .../Properties/Resources.Designer.cs | 9 + .../Properties/Resources.resx | 3 + .../WelsonJS.Launcher.csproj | 2 +- WelsonJS.Toolkit/WelsonJS.Launcher/app.config | 1 + 7 files changed, 386 insertions(+), 258 deletions(-) create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs delete mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs 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 @@ + From 6d3fb3db0e41bc0c401f20a8ba13af075df3a2e5 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Thu, 4 Dec 2025 16:08:25 +0900 Subject: [PATCH 2/6] Update AssemblyLoader.cs comment an unused variables --- WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs index 83de71a..bf6572d 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs @@ -49,9 +49,9 @@ namespace WelsonJS.Launcher 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 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"); From eae040530e075711f68a69d8e58b8d2cee082e2a Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Thu, 4 Dec 2025 16:12:47 +0900 Subject: [PATCH 3/6] Code signing is required Code signing is required --- .../WelsonJS.Launcher/Properties/Resources.Designer.cs | 9 --------- .../WelsonJS.Launcher/Properties/Resources.resx | 3 --- WelsonJS.Toolkit/WelsonJS.Launcher/app.config | 1 - 3 files changed, 13 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs index 0c6cea4..b5f61ae 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs @@ -342,15 +342,6 @@ namespace WelsonJS.Launcher.Properties { } } - /// - /// false과(와) 유사한 지역화된 문자열을 찾습니다. - /// - internal static string NativeRequireSigned { - get { - return ResourceManager.GetString("NativeRequireSigned", resourceCulture); - } - } - /// /// https://github.com/gnh1201/welsonjs과(와) 유사한 지역화된 문자열을 찾습니다. /// diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx index d0d2661..d5d208a 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx @@ -208,9 +208,6 @@ 5 - - false - true diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config index 59b2fa6..ce96d91 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config @@ -25,7 +25,6 @@ - From d2be6b116df46ec72a10c5f7bd324c4a304c7480 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Thu, 4 Dec 2025 16:21:47 +0900 Subject: [PATCH 4/6] Improve the loader policy Improve the loader policy --- .../WelsonJS.Launcher/AssemblyLoader.cs | 13 +++++++++++-- WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs index bf6572d..77f9fe6 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs @@ -29,13 +29,16 @@ namespace WelsonJS.Launcher /// Must be set before Register() or LoadNativeModules(). /// public static string BaseUrl { get; set; } = null; + public static ICompatibleLogger Logger { 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(); + private static readonly HttpClient Http = new HttpClient + { + Timeout = TimeSpan.FromSeconds(300) // 5 minutes + }; // -------------------- kernel32 native loading -------------------- @@ -150,6 +153,12 @@ namespace WelsonJS.Launcher throw new InvalidOperationException("AssemblyLoader.BaseUrl must be configured before Register()."); } + if (!BaseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + Logger.Error("AssemblyLoader.BaseUrl must use HTTPS for security."); + throw new InvalidOperationException("AssemblyLoader.BaseUrl must use HTTPS."); + } + AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve; _registered = true; diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index 77fc01f..bfac5b5 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -34,6 +34,7 @@ namespace WelsonJS.Launcher // load external assemblies AssemblyLoader.BaseUrl = GetAppConfig("AssemblyBaseUrl"); + AssemblyLoader.Logger = _logger; AssemblyLoader.Register(); AssemblyLoader.LoadNativeModules("ChakraCore", new Version(1, 13, 0, 0), new[] { "ChakraCore.dll" }); From cb41c92eab6616c75688091cc6d8e346651c8c71 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Thu, 4 Dec 2025 18:06:47 +0900 Subject: [PATCH 5/6] Improve the loader policy Improve the loader policy --- .../WelsonJS.Launcher/AssemblyLoader.cs | 79 +++++++++++++------ 1 file changed, 57 insertions(+), 22 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs index 77f9fe6..c647ddb 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs @@ -144,25 +144,28 @@ namespace WelsonJS.Launcher /// public static void Register() { - if (_registered) - return; - - if (string.IsNullOrWhiteSpace(BaseUrl)) + lock (SyncRoot) { - Logger.Error("AssemblyLoader.Register() called but BaseUrl is not set."); - throw new InvalidOperationException("AssemblyLoader.BaseUrl must be configured before 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()."); + } + + if (!BaseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + Logger.Error("AssemblyLoader.BaseUrl must use HTTPS for security."); + throw new InvalidOperationException("AssemblyLoader.BaseUrl must use HTTPS."); + } + + AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve; + _registered = true; + + Logger.Info("AssemblyLoader: AssemblyResolve handler registered."); } - - if (!BaseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) - { - Logger.Error("AssemblyLoader.BaseUrl must use HTTPS for security."); - throw new InvalidOperationException("AssemblyLoader.BaseUrl must use HTTPS."); - } - - AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve; - _registered = true; - - Logger.Info("AssemblyLoader: AssemblyResolve handler registered."); } @@ -186,8 +189,15 @@ namespace WelsonJS.Launcher string cacheDir = Path.Combine(appData, "WelsonJS", "assembly", ownerAssemblyName, versionString); Directory.CreateDirectory(cacheDir); - try { SetDllDirectory(cacheDir); } - catch { } + try + { + if (!SetDllDirectory(cacheDir)) + Logger.Warn("SetDllDirectory failed for: {0}", cacheDir); + } + catch (Exception ex) + { + Logger.Warn("SetDllDirectory threw exception: {0}", ex.Message); + } foreach (string raw in fileNames) { @@ -226,7 +236,18 @@ namespace WelsonJS.Launcher public static void LoadNativeModules(Assembly asm, IList fileNames) { + if (asm == null) + throw new ArgumentNullException(nameof(asm)); + if (fileNames == null) + throw new ArgumentNullException(nameof(fileNames)); + AssemblyName an = asm.GetName(); + + if (an == null) + throw new InvalidOperationException("Assembly.GetName() returned null."); + if (an.Name == null || an.Version == null) + throw new InvalidOperationException("Assembly name or version is missing."); + LoadNativeModules(an.Name, an.Version, fileNames); } @@ -302,7 +323,7 @@ namespace WelsonJS.Launcher res = Http.GetAsync(url).GetAwaiter().GetResult(); if (res.StatusCode == HttpStatusCode.NotFound) { - Logger.Warn("DownloadFile: 404 Not Found for {0}", url); + Logger.Warn("404 Not Found for {0}", url); return; } @@ -313,12 +334,26 @@ namespace WelsonJS.Launcher { s.CopyTo(fs); } + + if (!File.Exists(dest)) + { + throw new FileNotFoundException("File not found after download", dest); + } } catch (HttpRequestException ex) { - Logger.Error("DownloadFile: HTTP error for {0}: {1}", url, ex.Message); + Logger.Error("HTTP error for {0}: {1}", url, ex.Message); throw; } + catch (Exception ex) + { + Logger.Error("Error downloading {0}: {1}", url, ex.Message); + throw; + } + finally + { + res?.Dispose(); + } } @@ -331,7 +366,7 @@ namespace WelsonJS.Launcher name == "WindowsBase" || name == "PresentationCore" || name == "PresentationFramework" || - name.StartsWith(LoaderNamespace); + name.StartsWith(LoaderNamespace, StringComparison.OrdinalIgnoreCase); } From 7abf705c6e4230b9b0c37928a2fbd91c42c8d22a Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Thu, 4 Dec 2025 18:19:30 +0900 Subject: [PATCH 6/6] Improve the loader policy Improve the loader policy --- WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs index c647ddb..7e66011 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs @@ -223,7 +223,9 @@ namespace WelsonJS.Launcher IntPtr h = LoadLibrary(localPath); if (h == IntPtr.Zero) { - Logger.Error("LoadLibrary failed for {0}", localPath); + int errorCode = Marshal.GetLastWin32Error(); + Logger.Error("LoadLibrary failed for {0} with error code {1}", localPath, errorCode); + throw new InvalidOperationException($"Failed to load native module: {fileName} (error: {errorCode})"); } else { @@ -372,6 +374,12 @@ namespace WelsonJS.Launcher private static void EnsureSignedFileOrThrow(string path, string logicalName) { + if (!File.Exists(path)) + { + Logger.Error("File does not exist for signature verification: {0}", logicalName); + throw new FileNotFoundException("File not found for signature verification: " + logicalName, path); + } + FileSignatureStatus status = VerifySignature(path); if (status == FileSignatureStatus.Valid)