ExtensionLoader.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. using System.IO.Compression;
  2. using System.Reflection;
  3. using System.Runtime.InteropServices;
  4. using Newtonsoft.Json;
  5. using PixiEditor.Extensions.Metadata;
  6. using PixiEditor.Extensions.WasmRuntime;
  7. using PixiEditor.Platform;
  8. namespace PixiEditor.Extensions.Runtime;
  9. public class ExtensionLoader
  10. {
  11. private readonly Dictionary<string, OfficialExtensionData> _officialExtensionsKeys =
  12. new Dictionary<string, OfficialExtensionData>();
  13. public List<Extension> LoadedExtensions { get; } = new();
  14. public string PackagesPath { get; }
  15. public string UnpackedExtensionsPath { get; }
  16. private WasmRuntime.WasmRuntime _wasmRuntime = new WasmRuntime.WasmRuntime();
  17. public ExtensionLoader(string packagesPath, string unpackedExtensionsPath)
  18. {
  19. PackagesPath = packagesPath;
  20. UnpackedExtensionsPath = unpackedExtensionsPath;
  21. ValidateExtensionFolder();
  22. }
  23. public void AddOfficialExtension(string uniqueName, OfficialExtensionData data)
  24. {
  25. _officialExtensionsKeys.Add(uniqueName, data);
  26. }
  27. public void LoadExtensions()
  28. {
  29. foreach (var file in Directory.GetFiles(PackagesPath))
  30. {
  31. if (file.EndsWith(".pixiext"))
  32. {
  33. LoadExtension(file);
  34. }
  35. }
  36. }
  37. // Uncomment when PixiEditor.Core extension concept is implemented
  38. /*private void LoadCore()
  39. {
  40. Type entry = typeof(PixiEditorCoreExtension);
  41. Assembly assembly = entry.Assembly;
  42. var serializer = new JsonSerializer();
  43. Uri uri = new Uri("avares://PixiEditor.Core/extension.json");
  44. if (!AssetLoader.Exists(uri))
  45. {
  46. throw new FileNotFoundException("Core metadata not found", uri.ToString());
  47. }
  48. using var sr = new StreamReader(AssetLoader.Open(uri));
  49. using var jsonTextReader = new JsonTextReader(sr);
  50. ExtensionMetadata? metadata = serializer.Deserialize<ExtensionMetadata>(jsonTextReader);
  51. LoadExtensionFrom(assembly, entry, metadata);
  52. }*/
  53. public void InitializeExtensions(ExtensionServices apiServices)
  54. {
  55. try
  56. {
  57. foreach (var extension in LoadedExtensions)
  58. {
  59. extension.Initialize(apiServices);
  60. }
  61. }
  62. catch (Exception ex)
  63. {
  64. #if DEBUG
  65. throw;
  66. #endif
  67. // TODO: Log exception
  68. // Maybe it's not a good idea to send webhook exceptions in the extension loader
  69. //CrashHelper.SendExceptionInfoToWebhook(ex);
  70. }
  71. }
  72. public Extension? LoadExtension(string extension)
  73. {
  74. var extZip = ZipFile.OpenRead(extension);
  75. try
  76. {
  77. ExtensionMetadata metadata = ExtractMetadata(extZip);
  78. if (IsDifferentThanCached(metadata, extension))
  79. {
  80. UnpackExtension(extZip, metadata);
  81. }
  82. string extensionJson = Path.Combine(UnpackedExtensionsPath, metadata.UniqueName, "extension.json");
  83. if (!File.Exists(extensionJson))
  84. {
  85. return null;
  86. }
  87. return LoadExtensionFromCache(extensionJson);
  88. }
  89. catch (Exception ex)
  90. {
  91. return null;
  92. }
  93. }
  94. public void UnpackExtension(ZipArchive extZip, ExtensionMetadata metadata)
  95. {
  96. string extensionPath = Path.Combine(UnpackedExtensionsPath, metadata.UniqueName);
  97. if (Directory.Exists(extensionPath))
  98. {
  99. Directory.Delete(extensionPath, true);
  100. }
  101. extZip.ExtractToDirectory(extensionPath);
  102. }
  103. private ExtensionMetadata ExtractMetadata(ZipArchive extZip)
  104. {
  105. var metadataEntry = extZip.GetEntry("extension.json");
  106. if (metadataEntry == null)
  107. {
  108. throw new FileNotFoundException("Extension metadata not found");
  109. }
  110. using var stream = metadataEntry.Open();
  111. using var sr = new StreamReader(stream);
  112. using var jsonTextReader = new JsonTextReader(sr);
  113. var serializer = new JsonSerializer();
  114. return serializer.Deserialize<ExtensionMetadata>(jsonTextReader);
  115. }
  116. private bool IsDifferentThanCached(ExtensionMetadata metadata, string extension)
  117. {
  118. string extensionJson = Path.Combine(UnpackedExtensionsPath, metadata.UniqueName, "extension.json");
  119. if (!File.Exists(extensionJson))
  120. {
  121. return true;
  122. }
  123. string json = File.ReadAllText(extensionJson);
  124. ExtensionMetadata? cachedMetadata = JsonConvert.DeserializeObject<ExtensionMetadata>(json);
  125. if (cachedMetadata is null)
  126. {
  127. return true;
  128. }
  129. if (metadata.UniqueName != cachedMetadata.UniqueName)
  130. {
  131. return true;
  132. }
  133. bool isDifferent = metadata.Version != cachedMetadata.Version;
  134. if (isDifferent)
  135. {
  136. return true;
  137. }
  138. return PackageWriteTimeIsBigger(Path.Combine(UnpackedExtensionsPath, metadata.UniqueName), extension);
  139. }
  140. private bool PackageWriteTimeIsBigger(string unpackedDirectory, string extension)
  141. {
  142. DateTime extensionWriteTime = File.GetLastWriteTime(extension);
  143. DateTime unpackedWriteTime = Directory.GetLastWriteTime(unpackedDirectory);
  144. return extensionWriteTime > unpackedWriteTime;
  145. }
  146. private Extension LoadExtensionFromCache(string extension)
  147. {
  148. string json = File.ReadAllText(extension);
  149. try
  150. {
  151. var metadata = JsonConvert.DeserializeObject<ExtensionMetadata>(json);
  152. string directory = Path.GetDirectoryName(extension);
  153. ExtensionEntry? entry = GetEntry(directory);
  154. if (entry is null)
  155. {
  156. throw new NoEntryException(directory);
  157. }
  158. if (!ValidateMetadata(metadata, entry))
  159. {
  160. return null;
  161. }
  162. return LoadExtensionFrom(entry, metadata);
  163. }
  164. catch (JsonException)
  165. {
  166. #if DEBUG
  167. throw;
  168. #endif
  169. //MessageBox.Show(new LocalizedString("ERROR_INVALID_PACKAGE", packageJsonPath), "ERROR");
  170. }
  171. catch (ExtensionException ex)
  172. {
  173. #if DEBUG
  174. throw;
  175. #endif
  176. //MessageBox.Show(ex.DisplayMessage, "ERROR");
  177. }
  178. catch (Exception ex)
  179. {
  180. #if DEBUG
  181. throw;
  182. #endif
  183. //MessageBox.Show(new LocalizedString("ERROR_LOADING_PACKAGE", packageJsonPath), "ERROR");
  184. //CrashHelper.SendExceptionInfoToWebhook(ex);
  185. }
  186. return null;
  187. }
  188. private Extension LoadExtensionFrom(ExtensionEntry entry, ExtensionMetadata metadata)
  189. {
  190. var extension = LoadExtensionEntry(entry, metadata);
  191. extension.Load();
  192. LoadedExtensions.Add(extension);
  193. return extension;
  194. }
  195. private ExtensionEntry? GetEntry(string assemblyFolder)
  196. {
  197. string[] dlls = Directory.GetFiles(assemblyFolder, "*.dll");
  198. Assembly? entryAssembly = GetEntryAssembly(dlls, out Type extensionType);
  199. if (entryAssembly != null)
  200. {
  201. return new DllExtensionEntry(entryAssembly, extensionType);
  202. }
  203. string[] wasm = Directory.GetFiles(assemblyFolder, "*.wasm");
  204. WasmExtensionInstance? entryWasm = GetEntryWasm(wasm);
  205. if (entryWasm != null)
  206. {
  207. return new WasmExtensionEntry(entryWasm);
  208. }
  209. return null;
  210. }
  211. private bool ValidateMetadata(ExtensionMetadata metadata, ExtensionEntry assembly)
  212. {
  213. if (string.IsNullOrEmpty(metadata.UniqueName))
  214. {
  215. throw new MissingMetadataException("Description");
  216. }
  217. string fixedUniqueName = metadata.UniqueName.ToLower().Trim();
  218. if (fixedUniqueName.StartsWith("pixieditor".Trim(), StringComparison.OrdinalIgnoreCase))
  219. {
  220. if (!IsOfficialAssemblyLegit(fixedUniqueName, assembly))
  221. {
  222. throw new ForbiddenUniqueNameExtension();
  223. }
  224. if (!IsAdditionalContentInstalled(fixedUniqueName))
  225. {
  226. return false;
  227. }
  228. }
  229. // TODO: Validate if unique name is unique
  230. if (string.IsNullOrEmpty(metadata.DisplayName))
  231. {
  232. throw new MissingMetadataException("DisplayName");
  233. }
  234. if (string.IsNullOrEmpty(metadata.Version))
  235. {
  236. throw new MissingMetadataException("Version");
  237. }
  238. return true;
  239. }
  240. private bool IsAdditionalContentInstalled(string fixedUniqueName)
  241. {
  242. if (!_officialExtensionsKeys.ContainsKey(fixedUniqueName)) return false;
  243. AdditionalContentProduct? product = _officialExtensionsKeys[fixedUniqueName].Product;
  244. if (product == null) return true;
  245. return IPlatform.Current.AdditionalContentProvider?.IsContentInstalled(product.Value) ?? false;
  246. }
  247. private bool IsOfficialAssemblyLegit(string metadataUniqueName, ExtensionEntry entry)
  248. {
  249. if (entry == null) return false; // All official extensions must have a valid assembly
  250. if (!_officialExtensionsKeys.ContainsKey(metadataUniqueName)) return false;
  251. if (entry is DllExtensionEntry dllExtensionEntry)
  252. {
  253. return VerifyAssemblySignature(metadataUniqueName, dllExtensionEntry.Assembly);
  254. }
  255. if (entry is WasmExtensionEntry wasmExtensionEntry)
  256. {
  257. return true;
  258. //TODO: Verify wasm signature somehow
  259. //return VerifyAssemblySignature(metadataUniqueName, wasmExtensionEntry.Instance);
  260. }
  261. return false;
  262. }
  263. private bool VerifyAssemblySignature(string metadataUniqueName, Assembly assembly)
  264. {
  265. bool wasVerified = false;
  266. bool verified = StrongNameSignatureVerificationEx(assembly.Location, true, ref wasVerified);
  267. if (!verified || !wasVerified) return false;
  268. byte[]? assemblyPublicKey = assembly.GetName().GetPublicKey();
  269. if (assemblyPublicKey == null) return false;
  270. return PublicKeysMatch(assemblyPublicKey, _officialExtensionsKeys[metadataUniqueName].PublicKeyName);
  271. }
  272. private bool PublicKeysMatch(byte[] assemblyPublicKey, string pathToPublicKey)
  273. {
  274. Assembly currentAssembly = Assembly.GetExecutingAssembly();
  275. using Stream? stream =
  276. currentAssembly.GetManifestResourceStream(
  277. $"{currentAssembly.GetName().Name}.OfficialExtensions.{pathToPublicKey}");
  278. if (stream == null) return false;
  279. using MemoryStream memoryStream = new MemoryStream();
  280. stream.CopyTo(memoryStream);
  281. byte[] publicKey = memoryStream.ToArray();
  282. return assemblyPublicKey.SequenceEqual(publicKey);
  283. }
  284. //TODO: uhh, other platforms dumbass?
  285. [DllImport("mscoree.dll", CharSet = CharSet.Unicode)]
  286. static extern bool StrongNameSignatureVerificationEx(string wszFilePath, bool fForceVerification,
  287. ref bool pfWasVerified);
  288. private Extension LoadExtensionEntry(ExtensionEntry entry, ExtensionMetadata metadata)
  289. {
  290. Extension extension = entry.CreateExtension();
  291. extension.ProvideMetadata(metadata);
  292. return extension;
  293. }
  294. private Assembly? GetEntryAssembly(string[] dlls, out Type extensionType)
  295. {
  296. foreach (var dll in dlls)
  297. {
  298. try
  299. {
  300. var assembly = Assembly.LoadFrom(dll);
  301. extensionType = assembly.GetExportedTypes().FirstOrDefault(x => x.IsSubclassOf(typeof(Extension)));
  302. if (extensionType is not null)
  303. {
  304. return assembly;
  305. }
  306. }
  307. catch
  308. {
  309. // ignored
  310. }
  311. }
  312. extensionType = null;
  313. return null;
  314. }
  315. private WasmExtensionInstance? GetEntryWasm(string[] wasmFiles)
  316. {
  317. foreach (var wasm in wasmFiles)
  318. {
  319. try
  320. {
  321. WasmExtensionInstance instance = _wasmRuntime.LoadModule(wasm);
  322. return instance;
  323. }
  324. catch (Exception ex)
  325. {
  326. #if DEBUG
  327. throw;
  328. #endif
  329. }
  330. }
  331. return null;
  332. }
  333. private void ValidateExtensionFolder()
  334. {
  335. if (!Directory.Exists(PackagesPath))
  336. {
  337. Directory.CreateDirectory(PackagesPath);
  338. }
  339. if (!Directory.Exists(UnpackedExtensionsPath))
  340. {
  341. Directory.CreateDirectory(UnpackedExtensionsPath);
  342. }
  343. }
  344. public string? GetTypeId(Type id)
  345. {
  346. if (id.Assembly == Assembly.GetCallingAssembly())
  347. {
  348. return $"PixiEditor.{id.Name}";
  349. }
  350. foreach (var extension in LoadedExtensions)
  351. {
  352. Type? foundType = extension.Assembly.GetTypes().FirstOrDefault(x => x == id);
  353. if (foundType != null)
  354. {
  355. return $"{extension.Metadata.UniqueName}:{foundType.Name}";
  356. }
  357. }
  358. return null;
  359. }
  360. }
  361. public struct OfficialExtensionData
  362. {
  363. public string PublicKeyName { get; }
  364. public AdditionalContentProduct? Product { get; }
  365. public string? PurchaseLink { get; }
  366. public OfficialExtensionData(string publicKeyName, AdditionalContentProduct product, string? purchaseLink = null)
  367. {
  368. PublicKeyName = publicKeyName;
  369. Product = product;
  370. }
  371. }