Support relative path for external services, simplify the code in Services

This commit is contained in:
nicolas.dorier
2019-03-01 13:20:21 +09:00
parent de29d87487
commit 1d3f144d21
16 changed files with 415 additions and 511 deletions

View File

@@ -15,7 +15,6 @@ using Renci.SshNet;
using NBitcoin.DataEncoders; using NBitcoin.DataEncoders;
using BTCPayServer.SSH; using BTCPayServer.SSH;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
using Serilog.Events; using Serilog.Events;
namespace BTCPayServer.Configuration namespace BTCPayServer.Configuration
@@ -128,85 +127,7 @@ namespace BTCPayServer.Configuration
} }
} }
void externalLnd<T>(string code, string lndType) ExternalServices.Load(net.CryptoCode, conf);
{
var lightning = conf.GetOrDefault<string>(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<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
{
var spark = conf.GetOrDefault<string>($"{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<string>($"{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<string>($"{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));
}
}
} }
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray())); 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), .Select(p => (Name: p.p.Substring(0, p.SeparatorIndex),
Link: p.p.Substring(p.SeparatorIndex + 1)))) 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 string RootPath { get; set; }
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>(); public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
public Dictionary<string, string> ExternalServices { get; set; } = new Dictionary<string, string>();
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices(); public Dictionary<string, string> OtherExternalServices { get; set; } = new Dictionary<string, string>();
public ExternalServices ExternalServices { get; set; } = new ExternalServices();
public BTCPayNetworkProvider NetworkProvider { get; set; } public BTCPayNetworkProvider NetworkProvider { get; set; }
public string PostgresConnectionString public string PostgresConnectionString

View File

@@ -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; }
}
}

View File

@@ -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") { }
}
}

View File

@@ -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<string> ExtractAccessKey()
{
if (ConnectionString?.CookeFile == null)
throw new FormatException("Invalid connection string");
return await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile);
}
}
}

View File

@@ -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<string, ExternalService>
{
public IEnumerable<T> GetServices<T>(string cryptoCode) where T : ExternalService
{
if (!this.TryGetValue(cryptoCode.ToUpperInvariant(), out var services))
return Array.Empty<T>();
return services.OfType<T>();
}
}
public class ExternalService
{
}
}

View File

@@ -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<string> 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<string> 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");
}
}
}

View File

@@ -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; }
/// <summary>
/// Return a connectionString which does not depends on external resources or information like relative path or file path
/// </summary>
/// <returns></returns>
public async Task<ExternalConnectionString> 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;
}
}
}

View File

@@ -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<ExternalService>
{
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<string>(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
}
}

View File

@@ -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;
}
}
}

View File

@@ -49,6 +49,17 @@ namespace BTCPayServer.Controllers
} }
return macaroons; return macaroons;
} }
public Macaroons Clone()
{
return new Macaroons()
{
AdminMacaroon = AdminMacaroon,
InvoiceMacaroon = InvoiceMacaroon,
ReadonlyMacaroon = ReadonlyMacaroon
};
}
public Macaroon ReadonlyMacaroon { get; set; } public Macaroon ReadonlyMacaroon { get; set; }
public Macaroon InvoiceMacaroon { get; set; } public Macaroon InvoiceMacaroon { get; set; }

View File

@@ -26,7 +26,6 @@ using System.Threading.Tasks;
using Renci.SshNet; using Renci.SshNet;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
@@ -455,54 +454,10 @@ namespace BTCPayServer.Controllers
public IActionResult Services() public IActionResult Services()
{ {
var result = new ServicesViewModel(); var result = new ServicesViewModel();
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys) result.ExternalServices = _Options.ExternalServices;
foreach (var externalService in _Options.OtherExternalServices)
{ {
int i = 0; result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(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<ExternalSpark>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "Spark server",
Action = nameof(SparkService),
Index = i++,
});
}
foreach (var rtlService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalRTL>(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<ExternalCharge>(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()
{ {
Name = externalService.Key, Name = externalService.Key,
Link = this.Request.GetRelativePathOrAbsolute(externalService.Value) Link = this.Request.GetRelativePathOrAbsolute(externalService.Value)
@@ -510,7 +465,7 @@ namespace BTCPayServer.Controllers
} }
if (_Options.SSHSettings != null) if (_Options.SSHSettings != null)
{ {
result.ExternalServices.Add(new ServicesViewModel.ExternalService() result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
{ {
Name = "SSH", Name = "SSH",
Link = this.Url.Action(nameof(SSHService)) Link = this.Url.Action(nameof(SSHService))
@@ -519,160 +474,104 @@ namespace BTCPayServer.Controllers
return View(result); return View(result);
} }
[Route("server/services/lightning-charge/{cryptoCode}/{index}")] [Route("server/services/{serviceName}/{cryptoCode}")]
public async Task<IActionResult> LightningChargeServices(string cryptoCode, int index, bool showQR = false) public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null)
{ {
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud)) if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{ {
StatusMessage = $"Error: {cryptoCode} is not fully synched"; StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services)); return RedirectToAction(nameof(Services));
} }
var lightningCharge = _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault(); var service = _Options.ExternalServices.GetService(serviceName, cryptoCode);
if (lightningCharge == null) if (service == null)
{
return NotFound(); 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(); ChargeServiceViewModel vm = new ChargeServiceViewModel();
vm.Uri = lightningCharge.ToUri(false).AbsoluteUri; vm.Uri = connectionString.Server.AbsoluteUri;
vm.APIToken = lightningCharge.Password; vm.APIToken = connectionString.APIToken;
try return View(nameof(LightningChargeServices), vm);
{
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);
} }
[Route("server/services/spark/{cryptoCode}/{index}")] private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce)
public async Task<IActionResult> SparkService(string cryptoCode, int index, bool showQR = false)
{ {
return await LightningWalletServicesCore<ExternalSpark>(cryptoCode, showQR, "Spark Wallet");
}
[Route("server/services/rtl/{cryptoCode}/{index}")]
public async Task<IActionResult> RTLService(string cryptoCode, int index, bool showQR = false)
{
return await LightningWalletServicesCore<ExternalRTL>(cryptoCode, showQR, "Ride the Lightning Wallet");
}
private async Task<IActionResult> LightningWalletServicesCore<T>(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<T>(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<IActionResult> 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(); var model = new LndGrpcServicesViewModel();
if (external.ConnectionType == LightningConnectionType.LndGRPC) if (service.Type == ExternalServiceTypes.LNDGRPC)
{ {
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}"; model.Host = $"{connectionString.Server.DnsSafeHost}:{connectionString.Server.Port}";
model.SSL = external.BaseUri.Scheme == "https"; model.SSL = connectionString.Server.Scheme == "https";
model.ConnectionType = "GRPC"; model.ConnectionType = "GRPC";
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256"; 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"; 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 = connectionString.Macaroons?.AdminMacaroon?.Hex;
model.AdminMacaroon = macaroons?.AdminMacaroon?.Hex; model.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
model.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex; model.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
model.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
if (nonce != null) 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); var lnConfig = _LnConfigProvider.GetConfig(configKey);
if (lnConfig != null) 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}"; 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")] [Route("lnd-config/{configKey}/lnd.config")]
@@ -685,68 +584,62 @@ namespace BTCPayServer.Controllers
return Json(conf); return Json(conf);
} }
[Route("server/services/lnd/{cryptoCode}/{index}")] [Route("server/services/{serviceName}/{cryptoCode}")]
[HttpPost] [HttpPost]
public async Task<IActionResult> LndServicesPost(string cryptoCode, int index) public async Task<IActionResult> ServicePost(string serviceName, string cryptoCode)
{ {
var external = GetExternalLndConnectionString(cryptoCode, index); if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
if (external == null) {
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var service = _Options.ExternalServices.GetService(serviceName, cryptoCode);
if (service == null)
return NotFound(); 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(); LightningConfigurations confs = new LightningConfigurations();
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath); if (service.Type == ExternalServiceTypes.LNDGRPC)
if (external.ConnectionType == LightningConnectionType.LndGRPC)
{ {
LightningConfiguration grpcConf = new LightningConfiguration(); LightningConfiguration grpcConf = new LightningConfiguration();
grpcConf.Type = "grpc"; grpcConf.Type = "grpc";
grpcConf.Host = external.BaseUri.DnsSafeHost; grpcConf.Host = connectionString.Server.DnsSafeHost;
grpcConf.Port = external.BaseUri.Port; grpcConf.Port = connectionString.Server.Port;
grpcConf.SSL = external.BaseUri.Scheme == "https"; grpcConf.SSL = connectionString.Server.Scheme == "https";
confs.Configurations.Add(grpcConf); confs.Configurations.Add(grpcConf);
} }
else if (external.ConnectionType == LightningConnectionType.LndREST) else if (service.Type == ExternalServiceTypes.LNDRest)
{ {
var restconf = new LNDRestConfiguration(); var restconf = new LNDRestConfiguration();
restconf.Type = "lnd-rest"; restconf.Type = "lnd-rest";
restconf.Uri = external.BaseUri.AbsoluteUri; restconf.Uri = connectionString.Server.AbsoluteUri;
confs.Configurations.Add(restconf); confs.Configurations.Add(restconf);
} }
else else
throw new NotSupportedException(external.ConnectionType.ToString()); throw new NotSupportedException(service.Type.ToString());
var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1]; var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1];
commonConf.ChainType = _Options.NetworkType.ToString(); commonConf.ChainType = _Options.NetworkType.ToString();
commonConf.CryptoCode = cryptoCode; commonConf.CryptoCode = cryptoCode;
commonConf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon); commonConf.Macaroon = connectionString.Macaroon == null ? null : Encoders.Hex.EncodeData(connectionString.Macaroon);
commonConf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint); commonConf.CertificateThumbprint = connectionString.CertificateThumbprint == null ? null : connectionString.CertificateThumbprint;
commonConf.AdminMacaroon = macaroons?.AdminMacaroon?.Hex; commonConf.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
commonConf.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex; commonConf.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
commonConf.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex; commonConf.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
var nonce = RandomUtils.GetUInt32(); var nonce = RandomUtils.GetUInt32();
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce); var configKey = GetConfigKey("lnd", serviceName, cryptoCode, nonce);
_LnConfigProvider.KeepConfig(configKey, confs); _LnConfigProvider.KeepConfig(configKey, confs);
return RedirectToAction(nameof(LndServices), new { cryptoCode = cryptoCode, nonce = nonce }); return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
}
private LightningConnectionString GetExternalLndConnectionString(string cryptoCode, int index)
{
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(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;
} }
[Route("server/services/ssh")] [Route("server/services/ssh")]

View File

@@ -125,6 +125,12 @@ namespace BTCPayServer
return str; return str;
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) public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
{ {
@@ -227,19 +233,30 @@ namespace BTCPayServer
|| !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri; || !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri;
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl; return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
} }
public static string GetAbsoluteUriNoPathBase(this HttpRequest request, string url)
/// <summary>
/// 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)
/// </summary>
/// <param name="request"></param>
/// <param name="relativeOrAbsolte"></param>
/// <returns></returns>
public static Uri GetAbsoluteUriNoPathBase(this HttpRequest request, Uri relativeOrAbsolute = null)
{ {
bool isRelative = if (relativeOrAbsolute == null)
(url.Length > 0 && url[0] == '/')
|| !new Uri(url, UriKind.RelativeOrAbsolute).IsAbsoluteUri;
if (isRelative && (url.Length == 0 || url[0] != '/'))
{ {
url = $"/{url}"; return new Uri(string.Concat(
}
return isRelative ? string.Concat(
request.Scheme, 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) public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)

View File

@@ -43,7 +43,7 @@ namespace BTCPayServer.HostedServices
public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary) 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 != null &&
summary.Status.IsFullySynched; summary.Status.IsFullySynched;
} }

View File

@@ -2,27 +2,20 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Configuration.External; using BTCPayServer.Configuration;
namespace BTCPayServer.Models.ServerViewModels namespace BTCPayServer.Models.ServerViewModels
{ {
public class ServicesViewModel 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 Name { get; set; }
public string Link { get; set; } public string Link { get; set; }
} }
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();
public List<ExternalService> ExternalServices { get; set; } = new List<ExternalService>(); public List<ExternalService> ExternalServices { get; set; } = new List<ExternalService>();
public List<OtherExternalService> OtherExternalServices { get; set; } = new List<OtherExternalService>();
} }
} }

View File

@@ -31,7 +31,7 @@
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true", "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_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_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_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_DISABLE-REGISTRATION": "false", "BTCPAY_DISABLE-REGISTRATION": "false",
"ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_ENVIRONMENT": "Development",

View File

@@ -17,7 +17,7 @@
<div class="col-md-8"> <div class="col-md-8">
<h4>Crypto services</h4> <h4>Crypto services</h4>
<div class="form-group"> <div class="form-group">
<span>You can get access here to LND (gRPC, Rest) services exposed by your server</span> <span>You can get access here to services exposed by your server</span>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -30,13 +30,13 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var lnd in Model.LNDServices) @foreach (var s in Model.ExternalServices)
{ {
<tr> <tr>
<td>@lnd.Crypto</td> <td>@s.CryptoCode</td>
<td>@lnd.Type</td> <td>@s.DisplayName</td>
<td style="text-align:right"> <td style="text-align:right">
<a asp-action="@lnd.Action" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a> <a asp-action="Service" asp-route-serviceName="@s.ServiceName" asp-route-cryptoCode="@s.CryptoCode">See information</a>
</td> </td>
</tr> </tr>
} }
@@ -46,7 +46,7 @@
</div> </div>
</div> </div>
@if (Model.ExternalServices.Count != 0) @if (Model.OtherExternalServices.Count != 0)
{ {
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8">
@@ -63,7 +63,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var s in Model.ExternalServices) @foreach (var s in Model.OtherExternalServices)
{ {
<tr> <tr>
<td>@s.Name</td> <td>@s.Name</td>