using System; using System.Collections; using System.Collections.Generic; 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.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Models; using BTCPayServer.Configuration; using BTCPayServer.Data; using BTCPayServer.Lightning; using BTCPayServer.Logging; using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NBitcoin; using BTCPayServer.BIP78.Sender; using NBitcoin.Payment; using NBitpayClient; using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json.Linq; using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo; namespace BTCPayServer { public static class Extensions { public static bool TryGetPayjoinEndpoint(this BitcoinUrlBuilder bip21, out Uri endpoint) { endpoint = bip21.UnknowParameters.TryGetValue($"{PayjoinClient.BIP21EndpointKey}", out var uri) ? new Uri(uri, UriKind.Absolute) : null; return endpoint != null; } public static bool IsValidFileName(this string fileName) { return !fileName.ToCharArray().Any(c => Path.GetInvalidFileNameChars().Contains(c) || c == Path.AltDirectorySeparatorChar || c == Path.DirectorySeparatorChar || c == Path.PathSeparator || c == '\\'); } public static bool IsSafe(this LightningConnectionString connectionString) { if (connectionString.CookieFilePath != null || connectionString.MacaroonDirectoryPath != null || connectionString.MacaroonFilePath != null) return false; var uri = connectionString.BaseUri; if (uri.Scheme.Equals("unix", StringComparison.OrdinalIgnoreCase)) return false; if (!NBitcoin.Utils.TryParseEndpoint(uri.DnsSafeHost, 80, out var endpoint)) return false; return !Extensions.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 Truncate(this string value, int maxLength) { if (string.IsNullOrEmpty(value)) return value; return value.Length <= maxLength ? value : value.Substring(0, maxLength); } 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($"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}"); return builder.ToString(); } public static decimal RoundUp(decimal value, int precision) { 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; } public static bool HasStatusMessage(this ITempDataDictionary tempData) { return (tempData.Peek(WellKnownTempData.SuccessMessage) ?? tempData.Peek(WellKnownTempData.ErrorMessage) ?? tempData.Peek("StatusMessageModel")) != null; } public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info) { return new PaymentMethodId(info.CryptoCode, PaymentTypes.Parse(info.PaymentType)); } 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, bool accountedOnly) { return invoice.GetPayments(accountedOnly) .Where(p => p.GetPaymentMethodId()?.PaymentType == PaymentTypes.BTCLike) .Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData()) .Where(data => data != 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()); } #nullable enable public static IPayoutHandler? FindPayoutHandler(this IEnumerable handlers, PaymentMethodId paymentMethodId) { return handlers.FirstOrDefault(h => h.CanHandle(paymentMethodId)); } #nullable restore 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 string WithTrailingSlash(this string str) { if (str.EndsWith("/", StringComparison.InvariantCulture)) return str; return str + "/"; } public static string WithStartingSlash(this string str) { if (str.StartsWith("/", StringComparison.InvariantCulture)) return str; return $"/{str}"; } public static string WithoutEndingSlash(this string str) { if (str.EndsWith("/", StringComparison.InvariantCulture)) return str.Substring(0, str.Length - 1); return str; } 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) { if (server == null) throw new ArgumentNullException(nameof(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; } public static StatusMessageModel GetStatusMessageModel(this ITempDataDictionary tempData) { tempData.TryGetValue(WellKnownTempData.SuccessMessage, out var successMessage); tempData.TryGetValue(WellKnownTempData.ErrorMessage, out var errorMessage); tempData.TryGetValue("StatusMessageModel", out var model); if (successMessage != null || errorMessage != null) { var parsedModel = new StatusMessageModel(); parsedModel.Message = (string)successMessage ?? (string)errorMessage; if (successMessage != null) { parsedModel.Severity = StatusMessageModel.StatusSeverity.Success; } else { parsedModel.Severity = StatusMessageModel.StatusSeverity.Error; } return parsedModel; } else if (model != null && model is string str) { return JObject.Parse(str).ToObject(); } return null; } public static bool IsOnion(this HttpRequest request) { if (request?.Host.Host == null) return false; return request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase); } public static bool IsOnion(this Uri uri) { if (uri == null || !uri.IsAbsoluteUri) return false; return uri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase); } public static string GetAbsoluteRoot(this HttpRequest request) { return string.Concat( request.Scheme, "://", request.Host.ToUriComponent(), request.PathBase.ToUriComponent()); } public static Uri GetAbsoluteRootUri(this HttpRequest request) { return new Uri(request.GetAbsoluteRoot()); } public static string GetCurrentUrl(this HttpRequest request) { return string.Concat( request.Scheme, "://", request.Host.ToUriComponent(), request.PathBase.ToUriComponent(), request.Path.ToUriComponent()); } public static string GetCurrentPath(this HttpRequest request) { return string.Concat( request.PathBase.ToUriComponent(), request.Path.ToUriComponent()); } public static string GetCurrentPathWithQueryString(this HttpRequest request) { return request.PathBase + request.Path + request.QueryString; } /// /// If 'toto' and RootPath is 'rootpath' returns '/rootpath/toto' /// If 'toto' and RootPath is empty returns '/toto' /// /// /// /// public static string GetRelativePath(this HttpRequest request, string path) { if (path.Length > 0 && path[0] != '/') path = $"/{path}"; return string.Concat( request.PathBase.ToUriComponent(), path); } /// /// If 'https://example.com/toto' returns 'https://example.com/toto' /// If 'toto' and RootPath is 'rootpath' returns '/rootpath/toto' /// If 'toto' and RootPath is empty returns '/toto' /// /// /// /// public static string GetRelativePathOrAbsolute(this HttpRequest request, string path) { if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri) || uri.IsAbsoluteUri) return path; if (path.Length > 0 && path[0] != '/') path = $"/{path}"; return string.Concat( request.PathBase.ToUriComponent(), path); } public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl) { bool isRelative = (redirectUrl.Length > 0 && redirectUrl[0] == '/') || !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri; return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl; } /// /// Will return an absolute URL. /// If `relativeOrAsbolute` is absolute, returns it. /// If `relativeOrAsbolute` is relative, send absolute url based on the HOST of this request (without PathBase) /// /// /// /// public static Uri GetAbsoluteUriNoPathBase(this HttpRequest request, Uri relativeOrAbsolute = null) { if (relativeOrAbsolute == null) { return new Uri(string.Concat( request.Scheme, "://", request.Host.ToUriComponent()), UriKind.Absolute); } if (relativeOrAbsolute.IsAbsoluteUri) return relativeOrAbsolute; return new Uri(string.Concat( request.Scheme, "://", request.Host.ToUriComponent()) + relativeOrAbsolute.ToString().WithStartingSlash(), UriKind.Absolute); } 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 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; } 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 IActionResult RedirectToRecoverySeedBackup(this Controller controller, RecoverySeedBackupViewModel vm) { var redirectVm = new PostRedirectViewModel() { AspController = "Home", AspAction = "RecoverySeedBackup", Parameters = { new KeyValuePair("cryptoCode", vm.CryptoCode), new KeyValuePair("mnemonic", vm.Mnemonic), new KeyValuePair("passphrase", vm.Passphrase), new KeyValuePair("isStored", vm.IsStored ? "true" : "false"), new KeyValuePair("requireConfirm", vm.RequireConfirm ? "true" : "false"), new KeyValuePair("returnUrl", vm.ReturnUrl) } }; return controller.View("PostRedirect", redirectVm); } public static string ToSql(this IQueryable query) where TEntity : class { var enumerator = query.Provider.Execute>(query.Expression).GetEnumerator(); var relationalCommandCache = enumerator.Private("_relationalCommandCache"); var selectExpression = relationalCommandCache.Private("_selectExpression"); var factory = relationalCommandCache.Private("_querySqlGeneratorFactory"); var sqlGenerator = factory.Create(); var command = sqlGenerator.GetCommand(selectExpression); string sql = command.CommandText; return sql; } public static BTCPayNetworkProvider ConfigureNetworkProvider(this IConfiguration configuration, Logs logs) { var _networkType = DefaultConfiguration.GetNetworkType(configuration); var supportedChains = configuration.GetOrDefault("chains", "btc") .Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(t => t.ToUpperInvariant()).ToHashSet(); foreach (var c in supportedChains.ToList()) { if (new[] { "ETH", "USDT20", "FAU" }.Contains(c, StringComparer.OrdinalIgnoreCase)) { logs.Configuration.LogWarning($"'{c}' is not anymore supported, please remove it from 'chains'"); supportedChains.Remove(c); } } var networkProvider = new BTCPayNetworkProvider(_networkType); var filtered = networkProvider.Filter(supportedChains.ToArray()); #if ALTCOINS supportedChains.AddRange(filtered.GetAllElementsSubChains(networkProvider)); #endif #if !ALTCOINS var onlyBTC = supportedChains.Count == 1 && supportedChains.First() == "BTC"; if (!onlyBTC) throw new ConfigException($"This build of BTCPay Server does not support altcoins"); #endif var result = networkProvider.Filter(supportedChains.ToArray()); foreach (var chain in supportedChains) { if (result.GetNetwork(chain) == null) throw new ConfigException($"Invalid chains \"{chain}\""); } logs.Configuration.LogInformation( "Supported chains: " + String.Join(',', supportedChains.ToArray())); return result; } 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"); 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); } }