mirror of
https://github.com/gnh1201/welsonjs.git
synced 2025-12-08 15:24:07 +00:00
Merge pull request #358 from gnh1201/dev
Added the assembly loader with Azure Blob Storage
This commit is contained in:
commit
79d83f4346
420
WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs
Normal file
420
WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs
Normal file
|
|
@ -0,0 +1,420 @@
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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()
|
||||||
|
/// </summary>
|
||||||
|
public static class AssemblyLoader
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base URL for downloading managed/native binaries.
|
||||||
|
/// Example: https://catswords.blob.core.windows.net/welsonjs/packages
|
||||||
|
/// Must be set before Register() or LoadNativeModules().
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
{
|
||||||
|
Timeout = TimeSpan.FromSeconds(300) // 5 minutes
|
||||||
|
};
|
||||||
|
|
||||||
|
// -------------------- 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
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers AssemblyResolve to download and validate .NET assemblies.
|
||||||
|
/// </summary>
|
||||||
|
public static void Register()
|
||||||
|
{
|
||||||
|
lock (SyncRoot)
|
||||||
|
{
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads native modules associated with an assembly (explicit).
|
||||||
|
/// </summary>
|
||||||
|
public static void LoadNativeModules(string ownerAssemblyName, Version version, IList<string> 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
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
Logger.Info("Loaded native module: {0}", fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void LoadNativeModules(Assembly asm, IList<string> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 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("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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(dest))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("File not found after download", dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
public static void Init(
|
|
||||||
IEnumerable<string> dllNames,
|
|
||||||
string appDataSubdirectory,
|
|
||||||
ICompatibleLogger logger,
|
|
||||||
bool requireSigned = false,
|
|
||||||
Func<X509Certificate2, bool> 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<string>();
|
|
||||||
|
|
||||||
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<X509Certificate2, bool> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If requireSigned=false, returns true (no check).
|
|
||||||
/// If requireSigned=true, verifies Authenticode chain and optional custom validator.
|
|
||||||
/// </summary>
|
|
||||||
private static bool ValidateSignatureIfRequired(
|
|
||||||
string path,
|
|
||||||
bool requireSigned,
|
|
||||||
Func<X509Certificate2, bool> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -32,19 +32,11 @@ namespace WelsonJS.Launcher
|
||||||
// set up logger
|
// set up logger
|
||||||
_logger = new TraceLogger();
|
_logger = new TraceLogger();
|
||||||
|
|
||||||
// load native libraries
|
// load external assemblies
|
||||||
string appDataSubDirectory = "WelsonJS";
|
AssemblyLoader.BaseUrl = GetAppConfig("AssemblyBaseUrl");
|
||||||
bool requireSigned = string.Equals(
|
AssemblyLoader.Logger = _logger;
|
||||||
GetAppConfig("NativeRequireSigned"),
|
AssemblyLoader.Register();
|
||||||
"true",
|
AssemblyLoader.LoadNativeModules("ChakraCore", new Version(1, 13, 0, 0), new[] { "ChakraCore.dll" });
|
||||||
StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
NativeBootstrap.Init(
|
|
||||||
dllNames: new[] { "ChakraCore.dll" },
|
|
||||||
appDataSubdirectory: appDataSubDirectory,
|
|
||||||
logger: _logger,
|
|
||||||
requireSigned: requireSigned
|
|
||||||
);
|
|
||||||
|
|
||||||
// telemetry
|
// telemetry
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,15 @@ namespace WelsonJS.Launcher.Properties {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// https://catswords.blob.core.windows.net/welsonjs/packages과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||||
|
/// </summary>
|
||||||
|
internal static string AssemblyBaseUrl {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("AssemblyBaseUrl", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 과(와) 유사한 지역화된 문자열을 찾습니다.
|
/// 과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -333,15 +342,6 @@ namespace WelsonJS.Launcher.Properties {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// false과(와) 유사한 지역화된 문자열을 찾습니다.
|
|
||||||
/// </summary>
|
|
||||||
internal static string NativeRequireSigned {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("NativeRequireSigned", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// https://github.com/gnh1201/welsonjs과(와) 유사한 지역화된 문자열을 찾습니다.
|
/// https://github.com/gnh1201/welsonjs과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -208,9 +208,6 @@
|
||||||
<data name="ChromiumDevToolsTimeout" xml:space="preserve">
|
<data name="ChromiumDevToolsTimeout" xml:space="preserve">
|
||||||
<value>5</value>
|
<value>5</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="NativeRequireSigned" xml:space="preserve">
|
|
||||||
<value>false</value>
|
|
||||||
</data>
|
|
||||||
<data name="ChromiumAppMode" xml:space="preserve">
|
<data name="ChromiumAppMode" xml:space="preserve">
|
||||||
<value>true</value>
|
<value>true</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
@ -241,4 +238,7 @@
|
||||||
<data name="TelemetryEnabled" xml:space="preserve">
|
<data name="TelemetryEnabled" xml:space="preserve">
|
||||||
<value>true</value>
|
<value>true</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="AssemblyBaseUrl" xml:space="preserve">
|
||||||
|
<value>https://catswords.blob.core.windows.net/welsonjs/packages</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -88,12 +88,12 @@
|
||||||
<Reference Include="System.Xml.Linq" />
|
<Reference Include="System.Xml.Linq" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="AssemblyLoader.cs" />
|
||||||
<Compile Include="ICompatibleLogger.cs" />
|
<Compile Include="ICompatibleLogger.cs" />
|
||||||
<Compile Include="IResourceTool.cs" />
|
<Compile Include="IResourceTool.cs" />
|
||||||
<Compile Include="JsCore.cs" />
|
<Compile Include="JsCore.cs" />
|
||||||
<Compile Include="JsNative.cs" />
|
<Compile Include="JsNative.cs" />
|
||||||
<Compile Include="JsSerializer.cs" />
|
<Compile Include="JsSerializer.cs" />
|
||||||
<Compile Include="NativeBootstrap.cs" />
|
|
||||||
<Compile Include="ResourceTools\ImageColorPicker.cs" />
|
<Compile Include="ResourceTools\ImageColorPicker.cs" />
|
||||||
<Compile Include="ResourceTools\IpQuery.cs" />
|
<Compile Include="ResourceTools\IpQuery.cs" />
|
||||||
<Compile Include="ResourceTools\Settings.cs" />
|
<Compile Include="ResourceTools\Settings.cs" />
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,11 @@
|
||||||
<add key="IpQueryApiKey2" value=""/>
|
<add key="IpQueryApiKey2" value=""/>
|
||||||
<add key="IpQueryApiPrefix2" value="https://api.abuseipdb.com/api/v2/"/>
|
<add key="IpQueryApiPrefix2" value="https://api.abuseipdb.com/api/v2/"/>
|
||||||
<add key="DateTimeFormat" value="yyyy-MM-dd HH:mm:ss"/>
|
<add key="DateTimeFormat" value="yyyy-MM-dd HH:mm:ss"/>
|
||||||
<add key="NativeRequireSigned" value="false"/>
|
|
||||||
<add key="TelemetryProvider" value="posthog"/>
|
<add key="TelemetryProvider" value="posthog"/>
|
||||||
<add key="TelemetryApiKey" value="phc_pmRHJ0aVEhtULRT4ilexwCjYpGtE9VYRhlA05fwiYt8"/>
|
<add key="TelemetryApiKey" value="phc_pmRHJ0aVEhtULRT4ilexwCjYpGtE9VYRhlA05fwiYt8"/>
|
||||||
<add key="TelemetryBaseUrl" value="https://us.i.posthog.com"/>
|
<add key="TelemetryBaseUrl" value="https://us.i.posthog.com"/>
|
||||||
<add key="TelemetryEnabled" value="true"/>
|
<add key="TelemetryEnabled" value="true"/>
|
||||||
|
<add key="AssemblyBaseUrl" value="https://catswords.blob.core.windows.net/welsonjs/packages"/>
|
||||||
</appSettings>
|
</appSettings>
|
||||||
<startup>
|
<startup>
|
||||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user