diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index 81936205b..c5ecbbfa1 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -15,7 +15,6 @@ using Renci.SshNet; using NBitcoin.DataEncoders; using BTCPayServer.SSH; using BTCPayServer.Lightning; -using BTCPayServer.Configuration.External; using Serilog.Events; namespace BTCPayServer.Configuration @@ -128,85 +127,7 @@ namespace BTCPayServer.Configuration } } - void externalLnd(string code, string lndType) - { - var lightning = conf.GetOrDefault(code, string.Empty); - if (lightning.Length != 0) - { - if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error)) - { - Logs.Configuration.LogWarning($"Invalid setting {code}, " + Environment.NewLine + - $"lnd server: 'type={lndType};server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine + - $"lnd server: 'type={lndType};server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine + - $"Error: {error}" + Environment.NewLine + - "This service will not be exposed through BTCPay Server"); - } - else - { - var instanceType = typeof(T); - ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString)); - } - } - }; - - externalLnd($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc"); - externalLnd($"{net.CryptoCode}.external.lnd.rest", "lnd-rest"); - - { - var spark = conf.GetOrDefault($"{net.CryptoCode}.external.spark", string.Empty); - if (spark.Length != 0) - { - if (!SparkConnectionString.TryParse(spark, out var connectionString, out var error)) - { - Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine + - $"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine + - $"Error: {error}" + Environment.NewLine + - "This service will not be exposed through BTCPay Server"); - } - else - { - ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString)); - } - } - } - - { - var rtl = conf.GetOrDefault($"{net.CryptoCode}.external.rtl", string.Empty); - if (rtl.Length != 0) - { - if (!SparkConnectionString.TryParse(rtl, out var connectionString, out var error)) - { - Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.rtl, " + Environment.NewLine + - $"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine + - $"Error: {error}" + Environment.NewLine + - "This service will not be exposed through BTCPay Server"); - } - else - { - ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalRTL(connectionString)); - } - } - } - - var charge = conf.GetOrDefault($"{net.CryptoCode}.external.charge", string.Empty); - if (charge.Length != 0) - { - if (!LightningConnectionString.TryParse(charge, false, out var chargeConnectionString, out var chargeError)) - LightningConnectionString.TryParse("type=charge;" + charge, false, out chargeConnectionString, out chargeError); - - if (chargeConnectionString == null || chargeConnectionString.ConnectionType != LightningConnectionType.Charge) - { - Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.charge, " + Environment.NewLine + - $"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine + - $"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine + - $"Error: {chargeError ?? string.Empty}" + Environment.NewLine + - $"This service will not be exposed through BTCPay Server"); - } - else - { - ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString)); - } - } + ExternalServices.Load(net.CryptoCode, conf); } Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray())); @@ -220,7 +141,7 @@ namespace BTCPayServer.Configuration .Select(p => (Name: p.p.Substring(0, p.SeparatorIndex), Link: p.p.Substring(p.SeparatorIndex + 1)))) { - ExternalServices.AddOrReplace(service.Name, service.Link); + OtherExternalServices.AddOrReplace(service.Name, service.Link); } } @@ -325,9 +246,9 @@ namespace BTCPayServer.Configuration public string RootPath { get; set; } public Dictionary InternalLightningByCryptoCode { get; set; } = new Dictionary(); - public Dictionary ExternalServices { get; set; } = new Dictionary(); - public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices(); + public Dictionary OtherExternalServices { get; set; } = new Dictionary(); + public ExternalServices ExternalServices { get; set; } = new ExternalServices(); public BTCPayNetworkProvider NetworkProvider { get; set; } public string PostgresConnectionString diff --git a/BTCPayServer/Configuration/External/ExternalCharge.cs b/BTCPayServer/Configuration/External/ExternalCharge.cs deleted file mode 100644 index 03b8d5c86..000000000 --- a/BTCPayServer/Configuration/External/ExternalCharge.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Lightning; - -namespace BTCPayServer.Configuration.External -{ - public class ExternalCharge : ExternalService - { - public ExternalCharge(LightningConnectionString connectionString) - { - if (connectionString == null) - throw new ArgumentNullException(nameof(connectionString)); - ConnectionString = connectionString; - } - public LightningConnectionString ConnectionString { get; } - } -} diff --git a/BTCPayServer/Configuration/External/ExternalLnd.cs b/BTCPayServer/Configuration/External/ExternalLnd.cs deleted file mode 100644 index 838547df1..000000000 --- a/BTCPayServer/Configuration/External/ExternalLnd.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Lightning; - -namespace BTCPayServer.Configuration.External -{ - public abstract class ExternalLnd : ExternalService - { - public ExternalLnd(LightningConnectionString connectionString, string type) - { - ConnectionString = connectionString; - Type = type; - } - - public string Type { get; set; } - public LightningConnectionString ConnectionString { get; set; } - } - - public class ExternalLndGrpc : ExternalLnd - { - public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, "lnd-grpc") { } - } - - public class ExternalLndRest : ExternalLnd - { - public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, "lnd-rest") { } - } -} diff --git a/BTCPayServer/Configuration/External/ExternalRTL.cs b/BTCPayServer/Configuration/External/ExternalRTL.cs deleted file mode 100644 index e10d998b2..000000000 --- a/BTCPayServer/Configuration/External/ExternalRTL.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace BTCPayServer.Configuration.External -{ - public class ExternalRTL : ExternalService, IAccessKeyService - { - public SparkConnectionString ConnectionString { get; } - - public ExternalRTL(SparkConnectionString connectionString) - { - if (connectionString == null) - throw new ArgumentNullException(nameof(connectionString)); - ConnectionString = connectionString; - } - - public async Task ExtractAccessKey() - { - if (ConnectionString?.CookeFile == null) - throw new FormatException("Invalid connection string"); - return await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile); - } - } -} diff --git a/BTCPayServer/Configuration/External/ExternalService.cs b/BTCPayServer/Configuration/External/ExternalService.cs deleted file mode 100644 index c43a709b0..000000000 --- a/BTCPayServer/Configuration/External/ExternalService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Lightning; - -namespace BTCPayServer.Configuration.External -{ - public class ExternalServices : MultiValueDictionary - { - public IEnumerable GetServices(string cryptoCode) where T : ExternalService - { - if (!this.TryGetValue(cryptoCode.ToUpperInvariant(), out var services)) - return Array.Empty(); - return services.OfType(); - } - } - - public class ExternalService - { - } -} diff --git a/BTCPayServer/Configuration/External/ExternalSpark.cs b/BTCPayServer/Configuration/External/ExternalSpark.cs deleted file mode 100644 index e5da3572a..000000000 --- a/BTCPayServer/Configuration/External/ExternalSpark.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace BTCPayServer.Configuration.External -{ - public interface IAccessKeyService - { - SparkConnectionString ConnectionString { get; } - Task ExtractAccessKey(); - } - public class ExternalSpark : ExternalService, IAccessKeyService - { - public SparkConnectionString ConnectionString { get; } - - public ExternalSpark(SparkConnectionString connectionString) - { - if (connectionString == null) - throw new ArgumentNullException(nameof(connectionString)); - ConnectionString = connectionString; - } - - public async Task ExtractAccessKey() - { - if (ConnectionString?.CookeFile == null) - throw new FormatException("Invalid connection string"); - var cookie = (ConnectionString.CookeFile == "fake" - ? "fake:fake:fake" // Hacks for testing - : await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile)).Split(':'); - if (cookie.Length >= 3) - { - return cookie[2]; - } - throw new FormatException("Invalid cookiefile format"); - } - } -} diff --git a/BTCPayServer/Configuration/ExternalConnectionString.cs b/BTCPayServer/Configuration/ExternalConnectionString.cs new file mode 100644 index 000000000..1bf9fc3fe --- /dev/null +++ b/BTCPayServer/Configuration/ExternalConnectionString.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Controllers; + +namespace BTCPayServer.Configuration +{ + public class ExternalConnectionString + { + public Uri Server { get; set; } + public byte[] Macaroon { get; set; } + public Macaroons Macaroons { get; set; } + public string MacaroonFilePath { get; set; } + public string CertificateThumbprint { get; set; } + public string MacaroonDirectoryPath { get; set; } + public string APIToken { get; set; } + public string CookieFilePath { get; set; } + public string AccessKey { get; set; } + + /// + /// Return a connectionString which does not depends on external resources or information like relative path or file path + /// + /// + public async Task Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType) + { + var connectionString = this.Clone(); + // Transform relative URI into absolute URI + var serviceUri = connectionString.Server.IsAbsoluteUri ? connectionString.Server : ToRelative(absoluteUrlBase, connectionString.Server.ToString()); + if (!serviceUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) && + !serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase)) + { + throw new System.Security.SecurityException($"Insecure transport protocol to access this service, please use HTTPS or TOR"); + } + connectionString.Server = serviceUri; + + // Read the MacaroonFilePath + if (connectionString.MacaroonFilePath != null) + { + try + { + connectionString.Macaroon = await System.IO.File.ReadAllBytesAsync(connectionString.MacaroonFilePath); + connectionString.MacaroonFilePath = null; + } + catch (Exception ex) + { + throw new System.IO.FileNotFoundException("Macaroon not found", ex); + } + } + + // Read the MacaroonDirectory + if (connectionString.MacaroonDirectoryPath != null) + { + try + { + connectionString.Macaroons = await Macaroons.GetFromDirectoryAsync(connectionString.MacaroonDirectoryPath); + connectionString.MacaroonDirectoryPath = null; + } + catch (Exception ex) + { + throw new System.IO.DirectoryNotFoundException("Macaroon directory path not found", ex); + } + } + + // Read access key from cookie file + if (connectionString.CookieFilePath != null) + { + string cookieFileContent = null; + bool isFake = false; + try + { + cookieFileContent = await System.IO.File.ReadAllTextAsync(connectionString.CookieFilePath); + isFake = connectionString.CookieFilePath == "fake"; + connectionString.CookieFilePath = null; + } + catch (Exception ex) + { + throw new System.IO.FileNotFoundException("Cookie file path not found", ex); + } + if (serviceType == ExternalServiceTypes.RTL) + { + connectionString.AccessKey = cookieFileContent; + } + else if (serviceType == ExternalServiceTypes.Spark) + { + var cookie = (isFake ? "fake:fake:fake" // Hacks for testing + : cookieFileContent).Split(':'); + if (cookie.Length >= 3) + { + connectionString.AccessKey = cookie[2]; + } + throw new FormatException("Invalid cookiefile format"); + } + else if (serviceType == ExternalServiceTypes.Charge) + { + connectionString.APIToken = isFake ? "fake" : cookieFileContent; + } + } + return connectionString; + } + + private Uri ToRelative(Uri absoluteUrlBase, string path) + { + if (path.StartsWith('/')) + path = path.Substring(1); + return new Uri($"{absoluteUrlBase.AbsoluteUri.WithTrailingSlash()}{path}", UriKind.Absolute); + } + + public ExternalConnectionString Clone() + { + return new ExternalConnectionString() + { + MacaroonFilePath = MacaroonFilePath, + CertificateThumbprint = CertificateThumbprint, + Macaroon = Macaroon, + MacaroonDirectoryPath = MacaroonDirectoryPath, + Server = Server, + APIToken = APIToken, + CookieFilePath = CookieFilePath, + AccessKey = AccessKey, + Macaroons = Macaroons?.Clone() + }; + } + public static bool TryParse(string str, out ExternalConnectionString result, out string error) + { + if (str == null) + throw new ArgumentNullException(nameof(str)); + error = null; + result = null; + var resultTemp = new ExternalConnectionString(); + foreach(var kv in str.Split(';') + .Select(part => part.Split('=')) + .Where(kv => kv.Length == 2)) + { + switch (kv[0].ToLowerInvariant()) + { + case "server": + if (resultTemp.Server != null) + { + error = "Duplicated server attribute"; + return false; + } + if (!Uri.IsWellFormedUriString(kv[1], UriKind.RelativeOrAbsolute)) + { + error = "Invalid URI"; + return false; + } + resultTemp.Server = new Uri(kv[1], UriKind.RelativeOrAbsolute); + if (!resultTemp.Server.IsAbsoluteUri && (kv[1].Length == 0 || kv[1][0] != '/')) + resultTemp.Server = new Uri($"/{kv[1]}", UriKind.RelativeOrAbsolute); + break; + case "cookiefile": + case "cookiefilepath": + if (resultTemp.CookieFilePath != null) + { + error = "Duplicated cookiefile attribute"; + return false; + } + + resultTemp.CookieFilePath = kv[1]; + break; + case "macaroondirectorypath": + resultTemp.MacaroonDirectoryPath = kv[1]; + break; + case "certthumbprint": + resultTemp.CertificateThumbprint = kv[1]; + break; + case "macaroonfilepath": + resultTemp.MacaroonFilePath = kv[1]; + break; + case "api-token": + resultTemp.APIToken = kv[1]; + break; + case "access-key": + resultTemp.AccessKey = kv[1]; + break; + } + } + result = resultTemp; + return true; + } + } +} diff --git a/BTCPayServer/Configuration/ExternalService.cs b/BTCPayServer/Configuration/ExternalService.cs new file mode 100644 index 000000000..bfd3688b4 --- /dev/null +++ b/BTCPayServer/Configuration/ExternalService.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Lightning; +using Microsoft.Extensions.Configuration; + +namespace BTCPayServer.Configuration +{ + public class ExternalServices : List + { + public void Load(string cryptoCode, IConfiguration configuration) + { + Load(configuration, cryptoCode, "lndgrpc", ExternalServiceTypes.LNDGRPC, "Invalid setting {0}, " + Environment.NewLine + + "lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine + + "lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine + + "lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine + + "Error: {1}", + "LND (gRPC server)"); + Load(configuration, cryptoCode, "lndrest", ExternalServiceTypes.LNDRest, "Invalid setting {0}, " + Environment.NewLine + + "lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine + + "lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine + + "lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine + + "Error: {1}", + "LND (REST server)"); + Load(configuration, cryptoCode, "spark", ExternalServiceTypes.Spark, "Invalid setting {0}, " + Environment.NewLine + + $"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine + + "Error: {1}", + "C-Lightning (Spark server)"); + Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine + + $"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine + + "Error: {1}", + "LND (Ride the Lightning server)"); + Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine + + $"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine + + $"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine + + "Error: {1}", + "C-Lightning (Charge server)"); + } + + void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName) + { + var setting = $"{cryptoCode}.external.{serviceName}"; + var connStr = configuration.GetOrDefault(setting, string.Empty); + if (connStr.Length != 0) + { + if (!ExternalConnectionString.TryParse(connStr, out var connectionString, out var error)) + { + throw new ConfigException(string.Format(CultureInfo.InvariantCulture, errorMessage, setting, error)); + } + this.Add(new ExternalService() { Type = type, ConnectionString = connectionString, CryptoCode = cryptoCode, DisplayName = displayName, ServiceName = serviceName }); + } + } + + public ExternalService GetService(string serviceName, string cryptoCode) + { + return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) && + o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase)); + } + } + + public class ExternalService + { + public string DisplayName { get; set; } + public ExternalServiceTypes Type { get; set; } + public ExternalConnectionString ConnectionString { get; set; } + public string CryptoCode { get; set; } + public string ServiceName { get; set; } + } + + public enum ExternalServiceTypes + { + LNDRest, + LNDGRPC, + Spark, + RTL, + Charge + } +} diff --git a/BTCPayServer/Configuration/SparkConnectionString.cs b/BTCPayServer/Configuration/SparkConnectionString.cs deleted file mode 100644 index cba2d9657..000000000 --- a/BTCPayServer/Configuration/SparkConnectionString.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace BTCPayServer.Configuration -{ - public class SparkConnectionString - { - public Uri Server { get; private set; } - public string CookeFile { get; private set; } - - public static bool TryParse(string str, out SparkConnectionString result, out string error) - { - if (str == null) - throw new ArgumentNullException(nameof(str)); - error = null; - result = null; - var resultTemp = new SparkConnectionString(); - foreach(var kv in str.Split(';') - .Select(part => part.Split('=')) - .Where(kv => kv.Length == 2)) - { - switch (kv[0].ToLowerInvariant()) - { - case "server": - if (resultTemp.Server != null) - { - error = "Duplicated server attribute"; - return false; - } - if (!Uri.IsWellFormedUriString(kv[1], UriKind.RelativeOrAbsolute)) - { - error = "Invalid URI"; - return false; - } - resultTemp.Server = new Uri(kv[1], UriKind.RelativeOrAbsolute); - if (!resultTemp.Server.IsAbsoluteUri && (kv[1].Length == 0 || kv[1][0] != '/')) - resultTemp.Server = new Uri($"/{kv[1]}", UriKind.RelativeOrAbsolute); - break; - case "cookiefile": - case "cookiefilepath": - if (resultTemp.CookeFile != null) - { - error = "Duplicated cookiefile attribute"; - return false; - } - - resultTemp.CookeFile = kv[1]; - break; - default: - return false; - } - } - result = resultTemp; - return true; - } - } -} diff --git a/BTCPayServer/Controllers/Macaroons.cs b/BTCPayServer/Controllers/Macaroons.cs index 08eb369e8..bcc9b8c1d 100644 --- a/BTCPayServer/Controllers/Macaroons.cs +++ b/BTCPayServer/Controllers/Macaroons.cs @@ -49,6 +49,17 @@ namespace BTCPayServer.Controllers } return macaroons; } + + public Macaroons Clone() + { + return new Macaroons() + { + AdminMacaroon = AdminMacaroon, + InvoiceMacaroon = InvoiceMacaroon, + ReadonlyMacaroon = ReadonlyMacaroon + }; + } + public Macaroon ReadonlyMacaroon { get; set; } public Macaroon InvoiceMacaroon { get; set; } diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 627bc0dcb..3fc6d3c2e 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -26,7 +26,6 @@ using System.Threading.Tasks; using Renci.SshNet; using BTCPayServer.Logging; using BTCPayServer.Lightning; -using BTCPayServer.Configuration.External; using System.Runtime.CompilerServices; namespace BTCPayServer.Controllers @@ -455,54 +454,10 @@ namespace BTCPayServer.Controllers public IActionResult Services() { var result = new ServicesViewModel(); - foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys) + result.ExternalServices = _Options.ExternalServices; + foreach (var externalService in _Options.OtherExternalServices) { - int i = 0; - foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices(cryptoCode)) - { - result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel() - { - Crypto = cryptoCode, - Type = grpcService.Type, - Action = nameof(LndServices), - Index = i++, - }); - } - i = 0; - foreach (var sparkService in _Options.ExternalServicesByCryptoCode.GetServices(cryptoCode)) - { - result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel() - { - Crypto = cryptoCode, - Type = "Spark server", - Action = nameof(SparkService), - Index = i++, - }); - } - foreach (var rtlService in _Options.ExternalServicesByCryptoCode.GetServices(cryptoCode)) - { - result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel() - { - Crypto = cryptoCode, - Type = "Ride the Lightning server (RTL)", - Action = nameof(RTLService), - Index = i++, - }); - } - foreach (var chargeService in _Options.ExternalServicesByCryptoCode.GetServices(cryptoCode)) - { - result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel() - { - Crypto = cryptoCode, - Type = "Lightning charge server", - Action = nameof(LightningChargeServices), - Index = i++, - }); - } - } - foreach (var externalService in _Options.ExternalServices) - { - result.ExternalServices.Add(new ServicesViewModel.ExternalService() + result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService() { Name = externalService.Key, Link = this.Request.GetRelativePathOrAbsolute(externalService.Value) @@ -510,7 +465,7 @@ namespace BTCPayServer.Controllers } if (_Options.SSHSettings != null) { - result.ExternalServices.Add(new ServicesViewModel.ExternalService() + result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService() { Name = "SSH", Link = this.Url.Action(nameof(SSHService)) @@ -519,160 +474,104 @@ namespace BTCPayServer.Controllers return View(result); } - [Route("server/services/lightning-charge/{cryptoCode}/{index}")] - public async Task LightningChargeServices(string cryptoCode, int index, bool showQR = false) + [Route("server/services/{serviceName}/{cryptoCode}")] + public async Task Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null) { if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud)) { StatusMessage = $"Error: {cryptoCode} is not fully synched"; return RedirectToAction(nameof(Services)); } - var lightningCharge = _Options.ExternalServicesByCryptoCode.GetServices(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault(); - if (lightningCharge == null) - { + var service = _Options.ExternalServices.GetService(serviceName, cryptoCode); + if (service == null) return NotFound(); - } + try + { + var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type); + switch (service.Type) + { + case ExternalServiceTypes.Charge: + return LightningChargeServices(service, connectionString, showQR); + case ExternalServiceTypes.RTL: + case ExternalServiceTypes.Spark: + if (connectionString.AccessKey == null) + { + StatusMessage = $"Error: The access key of the service is not set"; + return RedirectToAction(nameof(Services)); + } + LightningWalletServices vm = new LightningWalletServices(); + vm.ShowQR = showQR; + vm.WalletName = service.DisplayName; + vm.ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}"; + return View("LightningWalletServices", vm); + case ExternalServiceTypes.LNDGRPC: + case ExternalServiceTypes.LNDRest: + return LndServices(service, connectionString, nonce); + default: + throw new NotSupportedException(service.Type.ToString()); + } + } + catch (Exception ex) + { + StatusMessage = $"Error: {ex.Message}"; + return RedirectToAction(nameof(Services)); + } + } + + private IActionResult LightningChargeServices(ExternalService service, ExternalConnectionString connectionString, bool showQR = false) + { ChargeServiceViewModel vm = new ChargeServiceViewModel(); - vm.Uri = lightningCharge.ToUri(false).AbsoluteUri; - vm.APIToken = lightningCharge.Password; - try - { - if (string.IsNullOrEmpty(vm.APIToken) && lightningCharge.CookieFilePath != null) - { - if (lightningCharge.CookieFilePath != "fake") - vm.APIToken = await System.IO.File.ReadAllTextAsync(lightningCharge.CookieFilePath); - else - vm.APIToken = "fake"; - } - var builder = new UriBuilder(lightningCharge.ToUri(false)); - builder.UserName = "api-token"; - builder.Password = vm.APIToken; - vm.AuthenticatedUri = builder.ToString(); - } - catch (Exception ex) - { - StatusMessage = $"Error: {ex.Message}"; - return RedirectToAction(nameof(Services)); - } - return View(vm); + vm.Uri = connectionString.Server.AbsoluteUri; + vm.APIToken = connectionString.APIToken; + return View(nameof(LightningChargeServices), vm); } - [Route("server/services/spark/{cryptoCode}/{index}")] - public async Task SparkService(string cryptoCode, int index, bool showQR = false) + private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce) { - return await LightningWalletServicesCore(cryptoCode, showQR, "Spark Wallet"); - } - [Route("server/services/rtl/{cryptoCode}/{index}")] - public async Task RTLService(string cryptoCode, int index, bool showQR = false) - { - return await LightningWalletServicesCore(cryptoCode, showQR, "Ride the Lightning Wallet"); - } - private async Task LightningWalletServicesCore(string cryptoCode, bool showQR, string walletName) where T : ExternalService, IAccessKeyService - { - if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud)) - { - StatusMessage = $"Error: {cryptoCode} is not fully synched"; - return RedirectToAction(nameof(Services)); - } - var external = _Options.ExternalServicesByCryptoCode.GetServices(cryptoCode).Where(c => c?.ConnectionString?.Server != null).FirstOrDefault(); - if (external == null) - { - return NotFound(); - } - - LightningWalletServices vm = new LightningWalletServices(); - vm.ShowQR = showQR; - vm.WalletName = walletName; - try - { - string serviceUri = null; - - if (external.ConnectionString.Server.IsAbsoluteUri) - { - serviceUri = external.ConnectionString.Server.AbsoluteUri; - } - else - { - serviceUri = this.Request.GetAbsoluteUriNoPathBase(external.ConnectionString.Server.ToString()); - } - AssertSecure(serviceUri); - vm.ServiceLink = $"{serviceUri}?access-key={await external.ExtractAccessKey()}"; - } - catch (Exception ex) - { - StatusMessage = $"Error: {ex.Message}"; - return RedirectToAction(nameof(Services)); - } - return View("LightningWalletServices", vm); - } - - private void AssertSecure(string serviceUri) - { - if (!Uri.TryCreate(serviceUri, UriKind.Absolute, out var uri)) - throw new System.Security.SecurityException("Invalid serviceUri"); - if(!uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) && - !uri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase)) - { - throw new System.Security.SecurityException("You can only access this service through https or Tor"); - } - } - - [Route("server/services/lnd/{cryptoCode}/{index}")] - public async Task LndServices(string cryptoCode, int index, uint? nonce) - { - if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud)) - { - StatusMessage = $"Error: {cryptoCode} is not fully synched"; - return RedirectToAction(nameof(Services)); - } - var external = GetExternalLndConnectionString(cryptoCode, index); - if (external == null) - return NotFound(); var model = new LndGrpcServicesViewModel(); - if (external.ConnectionType == LightningConnectionType.LndGRPC) + if (service.Type == ExternalServiceTypes.LNDGRPC) { - model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}"; - model.SSL = external.BaseUri.Scheme == "https"; + model.Host = $"{connectionString.Server.DnsSafeHost}:{connectionString.Server.Port}"; + model.SSL = connectionString.Server.Scheme == "https"; model.ConnectionType = "GRPC"; model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256"; } - else if (external.ConnectionType == LightningConnectionType.LndREST) + else if (service.Type == ExternalServiceTypes.LNDRest) { - model.Uri = external.BaseUri.AbsoluteUri; + model.Uri = connectionString.Server.AbsoluteUri; model.ConnectionType = "REST"; } - if (external.CertificateThumbprint != null) + if (connectionString.CertificateThumbprint != null) { - model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint); + model.CertificateThumbprint = connectionString.CertificateThumbprint; } - if (external.Macaroon != null) + if (connectionString.Macaroon != null) { - model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon); + model.Macaroon = Encoders.Hex.EncodeData(connectionString.Macaroon); } - var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath); - model.AdminMacaroon = macaroons?.AdminMacaroon?.Hex; - model.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex; - model.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex; + model.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex; + model.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex; + model.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex; if (nonce != null) { - var configKey = GetConfigKey("lnd", cryptoCode, index, nonce.Value); + var configKey = GetConfigKey("lnd", service.ServiceName, service.CryptoCode, nonce.Value); var lnConfig = _LnConfigProvider.GetConfig(configKey); if (lnConfig != null) { - model.QRCodeLink = $"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{configKey}/lnd.config"; + model.QRCodeLink = Url.Action(nameof(GetLNDConfig), new { configKey = configKey }); model.QRCode = $"config={model.QRCodeLink}"; } } - return View(model); + return View(nameof(LndServices), model); } - private static uint GetConfigKey(string type, string cryptoCode, int index, uint nonce) + private static uint GetConfigKey(string type, string serviceName, string cryptoCode, uint nonce) { - return (uint)HashCode.Combine(type, cryptoCode, index, nonce); + return (uint)HashCode.Combine(type, serviceName, cryptoCode, nonce); } [Route("lnd-config/{configKey}/lnd.config")] @@ -685,68 +584,62 @@ namespace BTCPayServer.Controllers return Json(conf); } - [Route("server/services/lnd/{cryptoCode}/{index}")] + [Route("server/services/{serviceName}/{cryptoCode}")] [HttpPost] - public async Task LndServicesPost(string cryptoCode, int index) + public async Task ServicePost(string serviceName, string cryptoCode) { - var external = GetExternalLndConnectionString(cryptoCode, index); - if (external == null) + if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud)) + { + StatusMessage = $"Error: {cryptoCode} is not fully synched"; + return RedirectToAction(nameof(Services)); + } + var service = _Options.ExternalServices.GetService(serviceName, cryptoCode); + if (service == null) return NotFound(); + + ExternalConnectionString connectionString = null; + try + { + connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type); + } + catch (Exception ex) + { + StatusMessage = $"Error: {ex.Message}"; + return RedirectToAction(nameof(Services)); + } + LightningConfigurations confs = new LightningConfigurations(); - var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath); - if (external.ConnectionType == LightningConnectionType.LndGRPC) + if (service.Type == ExternalServiceTypes.LNDGRPC) { LightningConfiguration grpcConf = new LightningConfiguration(); grpcConf.Type = "grpc"; - grpcConf.Host = external.BaseUri.DnsSafeHost; - grpcConf.Port = external.BaseUri.Port; - grpcConf.SSL = external.BaseUri.Scheme == "https"; + grpcConf.Host = connectionString.Server.DnsSafeHost; + grpcConf.Port = connectionString.Server.Port; + grpcConf.SSL = connectionString.Server.Scheme == "https"; confs.Configurations.Add(grpcConf); } - else if (external.ConnectionType == LightningConnectionType.LndREST) + else if (service.Type == ExternalServiceTypes.LNDRest) { var restconf = new LNDRestConfiguration(); restconf.Type = "lnd-rest"; - restconf.Uri = external.BaseUri.AbsoluteUri; + restconf.Uri = connectionString.Server.AbsoluteUri; confs.Configurations.Add(restconf); } else - throw new NotSupportedException(external.ConnectionType.ToString()); + throw new NotSupportedException(service.Type.ToString()); var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1]; commonConf.ChainType = _Options.NetworkType.ToString(); commonConf.CryptoCode = cryptoCode; - commonConf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon); - commonConf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint); - commonConf.AdminMacaroon = macaroons?.AdminMacaroon?.Hex; - commonConf.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex; - commonConf.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex; + commonConf.Macaroon = connectionString.Macaroon == null ? null : Encoders.Hex.EncodeData(connectionString.Macaroon); + commonConf.CertificateThumbprint = connectionString.CertificateThumbprint == null ? null : connectionString.CertificateThumbprint; + commonConf.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex; + commonConf.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex; + commonConf.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex; var nonce = RandomUtils.GetUInt32(); - var configKey = GetConfigKey("lnd", cryptoCode, index, nonce); + var configKey = GetConfigKey("lnd", serviceName, cryptoCode, nonce); _LnConfigProvider.KeepConfig(configKey, confs); - return RedirectToAction(nameof(LndServices), new { cryptoCode = cryptoCode, nonce = nonce }); - } - - private LightningConnectionString GetExternalLndConnectionString(string cryptoCode, int index) - { - var connectionString = _Options.ExternalServicesByCryptoCode.GetServices(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault(); - if (connectionString == null) - return null; - connectionString = connectionString.Clone(); - if (connectionString.MacaroonFilePath != null) - { - try - { - connectionString.Macaroon = System.IO.File.ReadAllBytes(connectionString.MacaroonFilePath); - connectionString.MacaroonFilePath = null; - } - catch - { - Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})"); - return null; - } - } - return connectionString; + return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce }); } [Route("server/services/ssh")] diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 67d47b343..b1e188940 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -125,6 +125,12 @@ namespace BTCPayServer return str; return str + "/"; } + public static string WithStartingSlash(this string str) + { + if (str.StartsWith("/", StringComparison.InvariantCulture)) + return str; + return $"/{str}"; + } public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value) { @@ -227,19 +233,30 @@ namespace BTCPayServer || !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri; return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl; } - public static string GetAbsoluteUriNoPathBase(this HttpRequest request, string url) + + /// + /// 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) { - bool isRelative = - (url.Length > 0 && url[0] == '/') - || !new Uri(url, UriKind.RelativeOrAbsolute).IsAbsoluteUri; - if (isRelative && (url.Length == 0 || url[0] != '/')) + if (relativeOrAbsolute == null) { - url = $"/{url}"; - } - return isRelative ? string.Concat( + return new Uri(string.Concat( request.Scheme, "://", - request.Host.ToUriComponent()) + url : url; + 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 IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf) diff --git a/BTCPayServer/HostedServices/NBXplorerWaiter.cs b/BTCPayServer/HostedServices/NBXplorerWaiter.cs index 4a1260886..f1308b421 100644 --- a/BTCPayServer/HostedServices/NBXplorerWaiter.cs +++ b/BTCPayServer/HostedServices/NBXplorerWaiter.cs @@ -43,7 +43,7 @@ namespace BTCPayServer.HostedServices public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary) { - return _Summaries.TryGetValue(cryptoCode, out summary) && + return _Summaries.TryGetValue(cryptoCode.ToUpperInvariant(), out summary) && summary.Status != null && summary.Status.IsFullySynched; } diff --git a/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs b/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs index 6e915f1a4..1cad93f1f 100644 --- a/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs +++ b/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs @@ -2,27 +2,20 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Configuration.External; +using BTCPayServer.Configuration; namespace BTCPayServer.Models.ServerViewModels { public class ServicesViewModel { - public class LNDServiceViewModel - { - public string Crypto { get; set; } - public string Type { get; set; } - public int Index { get; set; } - public string Action { get; internal set; } - } - public class ExternalService + public class OtherExternalService { public string Name { get; set; } public string Link { get; set; } } - public List LNDServices { get; set; } = new List(); public List ExternalServices { get; set; } = new List(); + public List OtherExternalServices { get; set; } = new List(); } } diff --git a/BTCPayServer/Properties/launchSettings.json b/BTCPayServer/Properties/launchSettings.json index c13ca06a7..22b7affe9 100644 --- a/BTCPayServer/Properties/launchSettings.json +++ b/BTCPayServer/Properties/launchSettings.json @@ -31,7 +31,7 @@ "BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true", "BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true", "BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake", - "BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/spark/btc/;cookiefilepath=fake", + "BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/mycharge/btc/;cookiefilepath=fake", "BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/", "BTCPAY_DISABLE-REGISTRATION": "false", "ASPNETCORE_ENVIRONMENT": "Development", diff --git a/BTCPayServer/Views/Server/Services.cshtml b/BTCPayServer/Views/Server/Services.cshtml index 8991585e6..52000fec2 100644 --- a/BTCPayServer/Views/Server/Services.cshtml +++ b/BTCPayServer/Views/Server/Services.cshtml @@ -17,7 +17,7 @@

Crypto services

- You can get access here to LND (gRPC, Rest) services exposed by your server + You can get access here to services exposed by your server
@@ -30,13 +30,13 @@ - @foreach (var lnd in Model.LNDServices) + @foreach (var s in Model.ExternalServices) { - @lnd.Crypto - @lnd.Type + @s.CryptoCode + @s.DisplayName - See information + See information } @@ -46,7 +46,7 @@
-@if (Model.ExternalServices.Count != 0) +@if (Model.OtherExternalServices.Count != 0) {
@@ -63,7 +63,7 @@ - @foreach (var s in Model.ExternalServices) + @foreach (var s in Model.OtherExternalServices) { @s.Name