using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Configuration; using BTCPayServer.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace BTCPayServer.Plugins { public class PluginService { private readonly IOptions _dataDirectories; private readonly PoliciesSettings _policiesSettings; private readonly PluginBuilderClient _pluginBuilderClient; public PluginService( IEnumerable btcPayServerPlugins, PluginBuilderClient pluginBuilderClient, IOptions dataDirectories, PoliciesSettings policiesSettings, BTCPayServerEnvironment env ) { LoadedPlugins = btcPayServerPlugins; Installed = btcPayServerPlugins.ToDictionary(p => p.Identifier, p => p.Version, StringComparer.OrdinalIgnoreCase); _pluginBuilderClient = pluginBuilderClient; _dataDirectories = dataDirectories; _policiesSettings = policiesSettings; Env = env; } public Dictionary Installed { get; set; } public IEnumerable LoadedPlugins { get; } public BTCPayServerEnvironment Env { get; } public Version GetVersionOfPendingInstall(string plugin) { var dirName = Path.Combine(_dataDirectories.Value.PluginDir, plugin); var manifestFileName = dirName + ".json"; if (!File.Exists(manifestFileName)) return null; var pluginManifest = JObject.Parse(File.ReadAllText(manifestFileName)).ToObject(); return pluginManifest.Version; } private string GetShortBtcpayVersion() => Env.Version.TrimStart('v').Split('+')[0]; public async Task GetRemotePlugins(string searchPluginName) { string btcpayVersion = GetShortBtcpayVersion(); var versions = await _pluginBuilderClient.GetPublishedVersions( btcpayVersion, _policiesSettings.PluginPreReleases, searchPluginName); var plugins = versions .Select(MapToAvailablePlugin) .Where(p => p is not null) .Select(p => p!) .ToList(); var listedIds = new HashSet( plugins.Select(p => p.Identifier), StringComparer.OrdinalIgnoreCase); var loadedToCheck = LoadedPlugins .Where(p => !p.SystemPlugin && !listedIds.Contains(p.Identifier)) .Select(p => new InstalledPluginRequest(p.Identifier, p.Version.ToString())) .ToList(); if (loadedToCheck.Count <= 0) return plugins.ToArray(); var updates = await _pluginBuilderClient.GetInstalledPluginsUpdates( btcpayVersion, _policiesSettings.PluginPreReleases, loadedToCheck); if (updates is { Length: > 0 }) { plugins.AddRange( updates.Select(MapToAvailablePlugin) .Where(p => p is not null) .Select(p => p!) ); } return plugins.ToArray(); } #nullable enable private AvailablePlugin? MapToAvailablePlugin(PublishedVersion publishedVersion) { if (publishedVersion.ManifestInfo is null) return null; var availablePlugin = publishedVersion.ManifestInfo.ToObject(); if (availablePlugin is null) throw new InvalidDataException($"Manifest deserialized to null BuildId: {publishedVersion.BuildId} PluginSlug: {publishedVersion.ProjectSlug}"); availablePlugin.Documentation = publishedVersion.Documentation; var buildInfo = publishedVersion.BuildInfo; var github = buildInfo?.GetGithubRepository(); if (buildInfo is not null && github is not null) { availablePlugin.Source = github.GetSourceUrl(buildInfo.gitCommit, buildInfo.pluginDir); availablePlugin.Author = github.Owner; availablePlugin.AuthorLink = $"https://github.com/{github.Owner}"; } availablePlugin.SystemPlugin = false; return availablePlugin; } #nullable restore public async Task DownloadRemotePlugin(string pluginIdentifier, string version, VersionCondition condition = null) { if (version is null) { string btcpayVersion = GetShortBtcpayVersion(); var versions = await _pluginBuilderClient.GetPluginVersionsForDownload(pluginIdentifier, btcpayVersion, _policiesSettings.PluginPreReleases, includeAllVersions: true); var potentialVersions = versions .Select(v => v.ManifestInfo?.ToObject()) .Where(v => v is not null) .Where(v => v.Identifier == pluginIdentifier) .Select(v => v.Version) .ToList(); if (potentialVersions.Count == 0) { throw new InvalidOperationException($"Plugin {pluginIdentifier} not found"); } if (condition is not null) { version = potentialVersions .OrderDescending() .FirstOrDefault(condition.IsFulfilled)?.ToString(); if (version is null) { throw new InvalidOperationException($"No version of plugin {pluginIdentifier} can satisfy condition {condition}"); } } } var dest = _dataDirectories.Value.PluginDir; var filedest = Path.Join(dest, pluginIdentifier + ".btcpay"); var filemanifestdest = Path.Join(dest, pluginIdentifier + ".json"); var pluginSelector = $"[{Uri.EscapeDataString(pluginIdentifier)}]"; version = Uri.EscapeDataString(version); Directory.CreateDirectory(Path.GetDirectoryName(filedest)); var url = $"api/v1/plugins/{pluginSelector}/versions/{version}/download"; var manifest = (await _pluginBuilderClient.GetPlugin(pluginSelector, version))?.ManifestInfo?.ToObject(); await File.WriteAllTextAsync(filemanifestdest, JsonConvert.SerializeObject(manifest, Formatting.Indented)); using var resp2 = await _pluginBuilderClient.HttpClient.GetAsync(url); await using var fs = new FileStream(filedest, FileMode.Create, FileAccess.ReadWrite); await resp2.Content.CopyToAsync(fs); await fs.FlushAsync(); return manifest; } public void InstallPlugin(string plugin) { PluginManager.QueueCommands(_dataDirectories.Value.PluginDir, ("install", plugin)); } public async Task UploadPlugin(IFormFile plugin) { var dest = _dataDirectories.Value.PluginDir; var filedest = Path.Combine(dest, plugin.FileName); Directory.CreateDirectory(Path.GetDirectoryName(filedest)); if (Path.GetExtension(filedest) == PluginManager.BTCPayPluginSuffix) { await using var stream = new FileStream(filedest, FileMode.Create); await plugin.CopyToAsync(stream); } } public void UninstallPlugin(string plugin) { var dest = _dataDirectories.Value.PluginDir; PluginManager.CancelCommands(dest, plugin); PluginManager.QueueCommands(dest, ("delete", plugin)); } public class AvailablePlugin { public string Identifier { get; set; } public string Name { get; set; } public Version Version { get; set; } public string Description { get; set; } public bool SystemPlugin { get; set; } = false; public IBTCPayServerPlugin.PluginDependency[] Dependencies { get; set; } = Array.Empty(); public string Documentation { get; set; } public string Source { get; set; } public string Author { get; set; } public string AuthorLink { get; set; } public void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices) { } public void Execute(IServiceCollection applicationBuilder) { } } class GithubFile { [JsonProperty("name")] public string Name { get; set; } [JsonProperty("sha")] public string Sha { get; set; } [JsonProperty("type")] public string Type { get; set; } [JsonProperty("path")] public string Path { get; set; } [JsonProperty("download_url")] public string DownloadUrl { get; set; } } public (string command, string plugin)[] GetPendingCommands() { return PluginManager.GetPendingCommands(_dataDirectories.Value.PluginDir); } public void CancelCommands(string plugin) { PluginManager.CancelCommands(_dataDirectories.Value.PluginDir, plugin); } public Dictionary GetDisabledPlugins() { return PluginManager.GetDisabledPlugins(_dataDirectories.Value.PluginDir); } } }