mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Support relative path for external services, simplify the code in Services
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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") { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
183
BTCPayServer/Configuration/ExternalConnectionString.cs
Normal file
183
BTCPayServer/Configuration/ExternalConnectionString.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
BTCPayServer/Configuration/ExternalService.cs
Normal file
80
BTCPayServer/Configuration/ExternalService.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
|
||||||
|
|
||||||
ChargeServiceViewModel vm = new ChargeServiceViewModel();
|
|
||||||
vm.Uri = lightningCharge.ToUri(false).AbsoluteUri;
|
|
||||||
vm.APIToken = lightningCharge.Password;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(vm.APIToken) && lightningCharge.CookieFilePath != null)
|
var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
|
||||||
|
switch (service.Type)
|
||||||
{
|
{
|
||||||
if (lightningCharge.CookieFilePath != "fake")
|
case ExternalServiceTypes.Charge:
|
||||||
vm.APIToken = await System.IO.File.ReadAllTextAsync(lightningCharge.CookieFilePath);
|
return LightningChargeServices(service, connectionString, showQR);
|
||||||
else
|
case ExternalServiceTypes.RTL:
|
||||||
vm.APIToken = "fake";
|
case ExternalServiceTypes.Spark:
|
||||||
}
|
if (connectionString.AccessKey == null)
|
||||||
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}";
|
StatusMessage = $"Error: The access key of the service is not set";
|
||||||
return RedirectToAction(nameof(Services));
|
return RedirectToAction(nameof(Services));
|
||||||
}
|
}
|
||||||
return View(vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("server/services/spark/{cryptoCode}/{index}")]
|
|
||||||
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();
|
LightningWalletServices vm = new LightningWalletServices();
|
||||||
vm.ShowQR = showQR;
|
vm.ShowQR = showQR;
|
||||||
vm.WalletName = walletName;
|
vm.WalletName = service.DisplayName;
|
||||||
try
|
vm.ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}";
|
||||||
{
|
return View("LightningWalletServices", vm);
|
||||||
string serviceUri = null;
|
case ExternalServiceTypes.LNDGRPC:
|
||||||
|
case ExternalServiceTypes.LNDRest:
|
||||||
if (external.ConnectionString.Server.IsAbsoluteUri)
|
return LndServices(service, connectionString, nonce);
|
||||||
{
|
default:
|
||||||
serviceUri = external.ConnectionString.Server.AbsoluteUri;
|
throw new NotSupportedException(service.Type.ToString());
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
serviceUri = this.Request.GetAbsoluteUriNoPathBase(external.ConnectionString.Server.ToString());
|
|
||||||
}
|
|
||||||
AssertSecure(serviceUri);
|
|
||||||
vm.ServiceLink = $"{serviceUri}?access-key={await external.ExtractAccessKey()}";
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
StatusMessage = $"Error: {ex.Message}";
|
StatusMessage = $"Error: {ex.Message}";
|
||||||
return RedirectToAction(nameof(Services));
|
return RedirectToAction(nameof(Services));
|
||||||
}
|
}
|
||||||
return View("LightningWalletServices", vm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AssertSecure(string serviceUri)
|
private IActionResult LightningChargeServices(ExternalService service, ExternalConnectionString connectionString, bool showQR = false)
|
||||||
{
|
{
|
||||||
if (!Uri.TryCreate(serviceUri, UriKind.Absolute, out var uri))
|
ChargeServiceViewModel vm = new ChargeServiceViewModel();
|
||||||
throw new System.Security.SecurityException("Invalid serviceUri");
|
vm.Uri = connectionString.Server.AbsoluteUri;
|
||||||
if(!uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
|
vm.APIToken = connectionString.APIToken;
|
||||||
!uri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
|
return View(nameof(LightningChargeServices), vm);
|
||||||
{
|
|
||||||
throw new System.Security.SecurityException("You can only access this service through https or Tor");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("server/services/lnd/{cryptoCode}/{index}")]
|
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce)
|
||||||
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")]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user