using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.WebSockets; using System.Reflection; using System.Security.Claims; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Services; using BTCPayServer.BIP78.Sender; using BTCPayServer.Configuration; using BTCPayServer.Data; using BTCPayServer.HostedServices; using BTCPayServer.Lightning; using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.NTag424; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Lightning; using BTCPayServer.Payouts; using BTCPayServer.Security; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Reporting; using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using NBitcoin; using NBitcoin.Payment; using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json; using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo; namespace BTCPayServer { public static class Extensions { /// /// Outputs a serializer which will serialize default and null members. /// This is useful for discovering the API. /// /// /// public static JsonSerializer ForAPI(this JsonSerializer settings) { var clone = new JsonSerializer() { CheckAdditionalContent = settings.CheckAdditionalContent, ConstructorHandling = settings.ConstructorHandling, ContractResolver = settings.ContractResolver, Culture = settings.Culture, DateFormatHandling = settings.DateFormatHandling, DateFormatString = settings.DateFormatString, DateParseHandling = settings.DateParseHandling, DateTimeZoneHandling = settings.DateTimeZoneHandling, DefaultValueHandling = settings.DefaultValueHandling, EqualityComparer = settings.EqualityComparer, FloatFormatHandling = settings.FloatFormatHandling, FloatParseHandling = settings.FloatParseHandling, Formatting = settings.Formatting, MaxDepth = settings.MaxDepth, MetadataPropertyHandling = settings.MetadataPropertyHandling, Context = settings.Context, MissingMemberHandling = settings.MissingMemberHandling, NullValueHandling = settings.NullValueHandling, ObjectCreationHandling = settings.ObjectCreationHandling, PreserveReferencesHandling = settings.PreserveReferencesHandling, ReferenceLoopHandling = settings.ReferenceLoopHandling, StringEscapeHandling = settings.StringEscapeHandling, TraceWriter = settings.TraceWriter, TypeNameAssemblyFormatHandling = settings.TypeNameAssemblyFormatHandling, SerializationBinder = settings.SerializationBinder, TypeNameHandling = settings.TypeNameHandling, ReferenceResolver = settings.ReferenceResolver }; foreach (var conv in settings.Converters) clone.Converters.Add(conv); clone.NullValueHandling = NullValueHandling.Include; clone.DefaultValueHandling = DefaultValueHandling.Include; return clone; } public static DerivationSchemeParser GetDerivationSchemeParser(this BTCPayNetwork network) { return new DerivationSchemeParser(network); } public static bool TryParseXpub(this DerivationSchemeParser derivationSchemeParser, string xpub, ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = false) { if (!electrum) { var isOD = Regex.Match(xpub, @"\(.*?\)").Success; try { var result = derivationSchemeParser.ParseOutputDescriptor(xpub); derivationSchemeSettings.AccountOriginal = xpub.Trim(); derivationSchemeSettings.AccountDerivation = result.Item1; derivationSchemeSettings.AccountKeySettings = result.Item2.Select((path, i) => new AccountKeySettings() { RootFingerprint = path?.MasterFingerprint, AccountKeyPath = path?.KeyPath, AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(derivationSchemeParser.Network) }).ToArray(); return true; } catch (Exception) { if (isOD) { return false; } // otherwise continue and try to parse input as xpub } } try { // Extract fingerprint and account key path from export formats that contain them. // Possible formats: [fingerprint/account_key_path]xpub, [fingerprint]xpub, xpub HDFingerprint? rootFingerprint = null; KeyPath accountKeyPath = null; var derivationRegex = new Regex(@"^(?:\[(\w+)(?:\/(.*?))?\])?(\w+)$", RegexOptions.IgnoreCase); var match = derivationRegex.Match(xpub.Trim()); if (match.Success) { if (!string.IsNullOrEmpty(match.Groups[1].Value)) rootFingerprint = HDFingerprint.Parse(match.Groups[1].Value); if (!string.IsNullOrEmpty(match.Groups[2].Value)) accountKeyPath = KeyPath.Parse(match.Groups[2].Value); if (!string.IsNullOrEmpty(match.Groups[3].Value)) xpub = match.Groups[3].Value; } derivationSchemeSettings.AccountOriginal = xpub.Trim(); derivationSchemeSettings.AccountDerivation = electrum ? derivationSchemeParser.ParseElectrum(derivationSchemeSettings.AccountOriginal) : derivationSchemeParser.Parse(derivationSchemeSettings.AccountOriginal); derivationSchemeSettings.AccountKeySettings = derivationSchemeSettings.AccountDerivation.GetExtPubKeys() .Select(key => new AccountKeySettings { AccountKey = key.GetWif(derivationSchemeParser.Network) }).ToArray(); if (derivationSchemeSettings.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit) derivationSchemeSettings.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation // apply initial matches if there were no results from parsing if (rootFingerprint != null && derivationSchemeSettings.AccountKeySettings[0].RootFingerprint == null) { derivationSchemeSettings.AccountKeySettings[0].RootFingerprint = rootFingerprint; } if (accountKeyPath != null && derivationSchemeSettings.AccountKeySettings[0].AccountKeyPath == null) { derivationSchemeSettings.AccountKeySettings[0].AccountKeyPath = accountKeyPath; } return true; } catch (Exception) { return false; } } public static CardKey CreatePullPaymentCardKey(this IssuerKey issuerKey, byte[] uid, int version, string pullPaymentId) { var data = Encoding.UTF8.GetBytes(pullPaymentId); return issuerKey.CreateCardKey(uid, version, data); } public static DateTimeOffset TruncateMilliSeconds(this DateTimeOffset dt) => new (dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, 0, dt.Offset); public static decimal? GetDue(this InvoiceCryptoInfo invoiceCryptoInfo) { if (invoiceCryptoInfo is null) return null; if (decimal.TryParse(invoiceCryptoInfo.Due, NumberStyles.Any, CultureInfo.InvariantCulture, out var v)) return v; return null; } public static Task Bufferize(this IFormFile formFile) { return BufferizedFormFile.Bufferize(formFile); } /// /// Unescape Uri string for %2F /// See details at: https://github.com/dotnet/aspnetcore/issues/14170#issuecomment-533342396 /// /// The Uri string. /// Unescaped back slash Uri string. public static string UnescapeBackSlashUriString(string uriString) { if (uriString == null) { return null; } return uriString.Replace("%2f", "%2F").Replace("%2F", "/"); } public static bool IsValidEmail(this string email) { if (string.IsNullOrEmpty(email)) { return false; } return MailboxAddressValidator.TryParse(email, out var ma) && ma.ToString() == ma.Address; } public static bool TryGetPayjoinEndpoint(this BitcoinUrlBuilder bip21, out Uri endpoint) { endpoint = bip21.UnknownParameters.TryGetValue($"{PayjoinClient.BIP21EndpointKey}", out var uri) ? new Uri(uri, UriKind.Absolute) : null; return endpoint != null; } public static Uri GetServerUri(this ILightningClient client) { var kv = LightningConnectionStringHelper.ExtractValues(client.ToString(), out _); return !kv.TryGetValue("server", out var server) ? null : new Uri(server, UriKind.Absolute); } public static string GetDisplayName(this ILightningClient client) { LightningConnectionStringHelper.ExtractValues(client.ToString(), out var type); var lncType = typeof(LightningConnectionType); var fields = lncType.GetFields(BindingFlags.Public | BindingFlags.Static); var field = fields.FirstOrDefault(f => f.GetValue(lncType)?.ToString() == type); if (field == null) return type; DisplayAttribute attr = field.GetCustomAttribute(); return attr?.Name ?? type; } public static bool IsSafe(this ILightningClient client) { var kv = LightningConnectionStringHelper.ExtractValues(client.ToString(), out _); if (kv.TryGetValue("cookiefilepath", out _) || kv.TryGetValue("macaroondirectorypath", out _) || kv.TryGetValue("macaroonfilepath", out _) ) return false; if (!kv.TryGetValue("server", out var server)) { return true; } var uri = new Uri(server, UriKind.Absolute); if (uri.Scheme.Equals("unix", StringComparison.OrdinalIgnoreCase)) return false; if (!Utils.TryParseEndpoint(uri.DnsSafeHost, 80, out _)) return false; return !IsLocalNetwork(uri.DnsSafeHost); } public static IQueryable Where(this Microsoft.EntityFrameworkCore.DbSet obj, System.Linq.Expressions.Expression> predicate) where TEntity : class { return System.Linq.Queryable.Where(obj, predicate); } public static string PrettyPrint(this TimeSpan expiration) { StringBuilder builder = new StringBuilder(); if (expiration.Days >= 1) builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture)); if (expiration.Hours >= 1) builder.Append(expiration.Hours.ToString("00", CultureInfo.InvariantCulture)); builder.Append(CultureInfo.InvariantCulture, $"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}"); return builder.ToString(); } public static decimal RoundUp(decimal value, int precision) { try { for (int i = 0; i < precision; i++) { value = value * 10m; } value = Math.Ceiling(value); for (int i = 0; i < precision; i++) { value = value / 10m; } return value; } catch (OverflowException) { return value; } } #nullable enable public static IServiceCollection AddDefaultTranslations(this IServiceCollection services, params string[] keyValues) { return services.AddDefaultTranslations(keyValues.Select(k => KeyValuePair.Create(k, string.Empty)).ToArray()); } public static IServiceCollection AddDefaultPrettyName(this IServiceCollection services, PaymentMethodId paymentMethodId, string defaultPrettyName) { services.AddSingleton(new PrettyNameProvider.UntranslatedPrettyName(paymentMethodId, defaultPrettyName)); return services.AddDefaultTranslations(KeyValuePair.Create(PrettyNameProvider.GetTranslationKey(paymentMethodId), defaultPrettyName)); } public static IServiceCollection AddDefaultTranslations(this IServiceCollection services, params KeyValuePair[] keyValues) { services.AddSingleton(new InMemoryDefaultTranslationProvider(keyValues)); return services; } #nullable restore public static IServiceCollection AddUIExtension(this IServiceCollection services, string location, string partialViewName) { #pragma warning disable CS0618 // Type or member is obsolete services.AddSingleton(new UIExtension(partialViewName, location)); #pragma warning restore CS0618 // Type or member is obsolete return services; } public static IServiceCollection AddReportProvider(this IServiceCollection services) where T : ReportProvider { services.AddSingleton(); services.AddSingleton(); return services; } public static IServiceCollection AddScheduledTask(this IServiceCollection services, TimeSpan every) where T : class, IPeriodicTask { services.AddSingleton(); services.AddTransient(o => new ScheduledTask(typeof(T), every)); return services; } public static async Task CloseSocket(this WebSocket webSocket) { try { if (webSocket.State == WebSocketState.Open) { using CancellationTokenSource cts = new CancellationTokenSource(); cts.CancelAfter(5000); await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token); } } catch { } finally { try { webSocket.Dispose(); } catch { } } } public static IEnumerable GetAllBitcoinPaymentData(this InvoiceEntity invoice, BitcoinLikePaymentHandler handler, bool accountedOnly) { return invoice.GetPayments(accountedOnly) .Select(p => p.GetDetails(handler)) .Where(p => p is not null); } public static async Task> GetTransactions(this BTCPayWallet client, uint256[] hashes, bool includeOffchain = false, CancellationToken cts = default(CancellationToken)) { hashes = hashes.Distinct().ToArray(); var transactions = hashes .Select(async o => await client.GetTransactionAsync(o, includeOffchain, cts)) .ToArray(); await Task.WhenAll(transactions).ConfigureAwait(false); return transactions.Select(t => t.Result).Where(t => t != null).ToDictionary(o => o.Transaction.GetHash()); } public static async Task UpdatePSBT(this ExplorerClientProvider explorerClientProvider, DerivationSchemeSettings derivationSchemeSettings, PSBT psbt) { var result = await explorerClientProvider.GetExplorerClient(psbt.Network.NetworkSet.CryptoCode).UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt, DerivationScheme = derivationSchemeSettings.AccountDerivation, AlwaysIncludeNonWitnessUTXO = true }); if (result == null) return null; derivationSchemeSettings.RebaseKeyPaths(result.PSBT); return result.PSBT; } public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value) { if (resp.HasStarted) return; resp.OnStarting(() => { SetHeader(resp, name, value); return Task.CompletedTask; }); } public static void SetHeader(this HttpResponse resp, string name, string value) { var existing = resp.Headers[name].FirstOrDefault(); if (existing != null && value == null) resp.Headers.Remove(name); else resp.Headers[name] = value; } public static bool IsLocalNetwork(string server) { ArgumentNullException.ThrowIfNull(server); if (Uri.CheckHostName(server) == UriHostNameType.Dns) { return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) || server.EndsWith(".local", StringComparison.OrdinalIgnoreCase) || server.EndsWith(".lan", StringComparison.OrdinalIgnoreCase) || server.IndexOf('.', StringComparison.OrdinalIgnoreCase) == -1; } if (IPAddress.TryParse(server, out var ip)) { return ip.IsLocal() || ip.IsRFC1918(); } return false; } #nullable enable public static LNURLPayPaymentHandler GetLNURLHandler(this PaymentMethodHandlerDictionary handlers, BTCPayNetwork network) { return handlers.GetLNURLHandler(network.CryptoCode); } public static LNURLPayPaymentHandler GetLNURLHandler(this PaymentMethodHandlerDictionary handlers, string cryptoCode) { var pmi = PaymentTypes.LNURL.GetPaymentMethodId(cryptoCode); var h = (LNURLPayPaymentHandler)handlers[pmi]; return h; } public static LightningLikePaymentHandler GetLightningHandler(this PaymentMethodHandlerDictionary handlers, BTCPayNetwork network) { return handlers.GetLightningHandler(network.CryptoCode); } public static LightningLikePaymentHandler GetLightningHandler(this PaymentMethodHandlerDictionary handlers, string cryptoCode) { var pmi = PaymentTypes.LN.GetPaymentMethodId(cryptoCode); var h = (LightningLikePaymentHandler)handlers[pmi]; return h; } public static BitcoinLikePaymentHandler? TryGetBitcoinHandler(this PaymentMethodHandlerDictionary handlers, BTCPayNetwork network) => handlers.TryGetBitcoinHandler(network.CryptoCode); public static BitcoinLikePaymentHandler? TryGetBitcoinHandler(this PaymentMethodHandlerDictionary handlers, string cryptoCode) => handlers.TryGetBitcoinHandler(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)); public static BitcoinLikePaymentHandler? TryGetBitcoinHandler(this PaymentMethodHandlerDictionary handlers, PaymentMethodId paymentMethodId) { if (handlers.TryGetValue(paymentMethodId, out var h) && h is BitcoinLikePaymentHandler b) return b; return null; } public static BitcoinLikePaymentHandler GetBitcoinHandler(this PaymentMethodHandlerDictionary handlers, BTCPayNetwork network) => handlers.GetBitcoinHandler(network.CryptoCode); public static BitcoinLikePaymentHandler GetBitcoinHandler(this PaymentMethodHandlerDictionary handlers, string cryptoCode) { var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode); var h = (BitcoinLikePaymentHandler)handlers[pmi]; return h; } public static BTCPayNetwork? TryGetNetwork(this HandlersDictionary handlers, TId id) where THandler : IHandler where TId : notnull { if (id is not null && handlers.TryGetValue(id, out var value) && value is IHasNetwork { Network: var n }) { return n; } return null; } public static BTCPayNetwork GetNetwork(this HandlersDictionary handlers, TId id) where THandler : IHandler where TId : notnull { return TryGetNetwork(handlers, id) ?? throw new KeyNotFoundException($"Network for {id} is not found"); } public static LightningPaymentMethodConfig? GetLightningConfig(this PaymentMethodHandlerDictionary handlers, Data.StoreData store, BTCPayNetwork network) { var config = store.GetPaymentMethodConfig(PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode)); if (config is null) return null; return handlers.GetLightningHandler(network).ParsePaymentMethodConfig(config); } public static DerivationStrategyBase? GetDerivationStrategy(this PaymentMethodHandlerDictionary handlers, InvoiceEntity invoice, BTCPayNetworkBase network) { var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode); if (!handlers.TryGetValue(pmi, out var handler)) return null; var prompt = invoice.GetPaymentPrompt(pmi); if (prompt?.Details is null) return null; var details = (BitcoinPaymentPromptDetails)handler.ParsePaymentPromptDetails(prompt.Details); return details.AccountDerivation; } #nullable restore public static bool IsOnion(this Uri uri) { if (uri == null || !uri.IsAbsoluteUri) return false; return uri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase); } public static string GetSIN(this ClaimsPrincipal principal) { return principal.Claims.Where(c => c.Type == Security.Bitpay.BitpayClaims.SIN).Select(c => c.Value).FirstOrDefault(); } public static void SetIsBitpayAPI(this HttpContext ctx, bool value) { NBitcoin.Extensions.TryAdd(ctx.Items, "IsBitpayAPI", value); } public static bool GetIsBitpayAPI(this HttpContext ctx) { return ctx.Items.TryGetValue("IsBitpayAPI", out object obj) && obj is bool b && b; } public static void SetBitpayAuth(this HttpContext ctx, (string Signature, String Id, String Authorization) value) { NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value); } public static bool TryGetBitpayAuth(this HttpContext ctx, out (string Signature, String Id, String Authorization) result) { if (ctx.Items.TryGetValue("BitpayAuth", out object obj)) { result = ((string Signature, String Id, String Authorization))obj; return true; } result = default; return false; } public static UserPrefsCookie GetUserPrefsCookie(this HttpContext ctx) { var prefCookie = new UserPrefsCookie(); ctx.Request.Cookies.TryGetValue(nameof(UserPrefsCookie), out var strPrefCookie); if (!string.IsNullOrEmpty(strPrefCookie)) { try { prefCookie = JsonConvert.DeserializeObject(strPrefCookie); } catch { /* ignore cookie deserialization failures */ } } return prefCookie; } public static void DeleteUserPrefsCookie(this HttpContext ctx) { ctx.Response.Cookies.Delete(nameof(UserPrefsCookie)); } private static void SetCurrentStoreId(this HttpContext ctx, string storeId) { var prefCookie = ctx.GetUserPrefsCookie(); if (prefCookie.CurrentStoreId != storeId) { prefCookie.CurrentStoreId = storeId; ctx.Response.Cookies.Append(nameof(UserPrefsCookie), JsonConvert.SerializeObject(prefCookie)); } } public static string GetCurrentStoreId(this HttpContext ctx) { return ctx.GetImplicitStoreId() ?? ctx.GetUserPrefsCookie()?.CurrentStoreId; } public static StoreData GetStoreData(this HttpContext ctx) { return ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData; } public static void SetStoreData(this HttpContext ctx, StoreData storeData) { ctx.Items["BTCPAY.STOREDATA"] = storeData; SetCurrentStoreId(ctx, storeData.Id); } public static StoreData[] GetStoresData(this HttpContext ctx) { return ctx.Items.TryGet("BTCPAY.STORESDATA") as StoreData[]; } public static void SetStoresData(this HttpContext ctx, StoreData[] storeData) { ctx.Items["BTCPAY.STORESDATA"] = storeData; } public static InvoiceEntity GetInvoiceData(this HttpContext ctx) { return ctx.Items.TryGet("BTCPAY.INVOICEDATA") as InvoiceEntity; } public static void SetInvoiceData(this HttpContext ctx, InvoiceEntity invoiceEntity) { ctx.Items["BTCPAY.INVOICEDATA"] = invoiceEntity; } public static PaymentRequestData GetPaymentRequestData(this HttpContext ctx) { return ctx.Items.TryGet("BTCPAY.PAYMENTREQUESTDATA") as PaymentRequestData; } public static void SetPaymentRequestData(this HttpContext ctx, PaymentRequestData paymentRequestData) { ctx.Items["BTCPAY.PAYMENTREQUESTDATA"] = paymentRequestData; } public static AppData GetAppData(this HttpContext ctx) { return ctx.Items.TryGet("BTCPAY.APPDATA") as AppData; } public static void SetAppData(this HttpContext ctx, AppData appData) { ctx.Items["BTCPAY.APPDATA"] = appData; } public static bool SupportChain(this IConfiguration conf, string cryptoCode) { var supportedChains = conf.GetOrDefault("chains", "btc") .Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(t => t.ToUpperInvariant()).ToHashSet(); return supportedChains.Contains(cryptoCode.ToUpperInvariant()); } public static IActionResult RedirectToRecoverySeedBackup(this Controller controller, RecoverySeedBackupViewModel vm) { var redirectVm = new PostRedirectViewModel { AspController = "UIHome", AspAction = "RecoverySeedBackup", FormParameters = { { "cryptoCode", vm.CryptoCode }, { "mnemonic", vm.Mnemonic }, { "passphrase", vm.Passphrase }, { "isStored", vm.IsStored ? "true" : "false" }, { "requireConfirm", vm.RequireConfirm ? "true" : "false" }, { "returnUrl", vm.ReturnUrl } } }; return controller.View("PostRedirect", redirectVm); } public static DataDirectories Configure(this DataDirectories dataDirectories, IConfiguration configuration) { var networkType = DefaultConfiguration.GetNetworkType(configuration); var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType); dataDirectories.DataDir = configuration["datadir"] ?? defaultSettings.DefaultDataDirectory; dataDirectories.PluginDir = configuration["plugindir"] ?? defaultSettings.DefaultPluginDirectory; dataDirectories.StorageDir = Path.Combine(dataDirectories.DataDir, Storage.Services.Providers.FileSystemStorage.FileSystemFileProviderService.LocalStorageDirectoryName); dataDirectories.TempStorageDir = Path.Combine(dataDirectories.StorageDir, "tmp"); dataDirectories.TempDir = Path.Combine(dataDirectories.DataDir, "tmp"); dataDirectories.LangsDir = Path.Combine(dataDirectories.DataDir, "Langs"); return dataDirectories; } private static object Private(this object obj, string privateField) => obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj); private static T Private(this object obj, string privateField) => (T)obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj); } }