Refactor how we handle and validate LN ConnectionStrings (#2314)

* Refactor how we handle and validate LN ConnectionStrings

* Migrate existing connection string to Internal Node if they are the same. Cleanup some obsolete fields

* Fix typos, remove duplicated method

* Add a InternalNodeRef to LightningSupportedPaymentMethod
This commit is contained in:
Nicolas Dorier 2021-03-02 11:11:58 +09:00 committed by GitHub
parent 49ae62b02e
commit 7e714f1ef8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 572 additions and 297 deletions

View file

@ -80,7 +80,7 @@ namespace BTCPayServer.Tests
tester.ActivateLightning();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.GrantAccess(true);
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
@ -287,7 +287,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.GrantAccess(true);
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
@ -876,7 +876,7 @@ normal:
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var networkBTC = networkProvider.GetNetwork("BTC");
var networkLTC = networkProvider.GetNetwork("LTC");

View file

@ -112,7 +112,7 @@ namespace BTCPayServer.Tests
if (UseLightning)
{
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
config.AppendLine($"btc.lightning={IntegratedLightning}");
var localLndBackupFile = Path.Combine(_Directory, "walletunlock.json");
File.Copy(TestUtils.GetTestDataFullPath("LndSeedBackup/walletunlock.json"), localLndBackupFile, true);
config.AppendLine($"btc.external.lndseedbackup={localLndBackupFile}");
@ -269,7 +269,7 @@ namespace BTCPayServer.Tests
public InvoiceRepository InvoiceRepository { get; private set; }
public StoreRepository StoreRepository { get; private set; }
public BTCPayNetworkProvider Networks { get; private set; }
public Uri IntegratedLightning { get; internal set; }
public string IntegratedLightning { get; internal set; }
public bool InContainer { get; internal set; }
public T GetService<T>()

View file

@ -109,7 +109,7 @@ namespace BTCPayServer.Tests
s.Server.ActivateLightning();
await s.StartAsync();
s.GoToRegister();
s.RegisterNewUser();
s.RegisterNewUser(true);
var store = s.CreateNewStore();
s.AddInternalLightningNode("BTC");
s.GoToStore(store.storeId, StoreNavPages.Checkout);

View file

@ -540,7 +540,7 @@ namespace BTCPayServer.Tests
}
}
private async Task AssertValidationError(string[] fields, Func<Task> act)
private async Task<GreenFieldValidationException> AssertValidationError(string[] fields, Func<Task> act)
{
var remainingFields = fields.ToHashSet();
var ex = await Assert.ThrowsAsync<GreenFieldValidationException>(act);
@ -550,6 +550,7 @@ namespace BTCPayServer.Tests
remainingFields.Remove(field);
}
Assert.Empty(remainingFields);
return ex;
}
private async Task AssertHttpError(int code, Func<Task> act)
@ -1112,7 +1113,6 @@ namespace BTCPayServer.Tests
merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
var merchantClient = await merchant.CreateClient($"{Policies.CanUseLightningNodeInStore}:{merchant.StoreId}");
var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(LightMoney.Satoshis(1_000), "hey", TimeSpan.FromSeconds(60)));
tester.PayTester.GetService<BTCPayServerEnvironment>().DevelopmentOverride = false;
// The default client is using charge, so we should not be able to query channels
var client = await user.CreateClient(Policies.CanUseInternalLightningNode);
@ -1291,33 +1291,86 @@ namespace BTCPayServer.Tests
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
var client = await user.CreateClient(Policies.CanModifyStoreSettings);
var viewOnlyClient = await user.CreateClient(Policies.CanViewStoreSettings);
tester.PayTester.GetService<BTCPayServerEnvironment>().DevelopmentOverride = false;
var store = await client.GetStore(user.StoreId);
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var admin2 = tester.NewAccount();
await admin2.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.CanModifyStoreSettings);
var admin2Client = await admin2.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
var viewOnlyClient = await admin.CreateClient(Policies.CanViewStoreSettings);
var store = await adminClient.GetStore(admin.StoreId);
Assert.Empty(await client.GetStoreLightningNetworkPaymentMethods(store.Id));
Assert.Empty(await adminClient.GetStoreLightningNetworkPaymentMethods(store.Id));
await AssertHttpError(403, async () =>
{
await viewOnlyClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData() { });
});
await AssertHttpError(404, async () =>
{
await client.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC");
await adminClient.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC");
});
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning, false);
await admin.RegisterLightningNodeAsync("BTC", false);
var method = await client.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC");
var method = await adminClient.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC");
await AssertHttpError(403, async () =>
{
await viewOnlyClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
});
await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await adminClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await AssertHttpError(404, async () =>
{
await client.GetStoreOnChainPaymentMethod(store.Id, "BTC");
await adminClient.GetStoreOnChainPaymentMethod(store.Id, "BTC");
});
// Let's verify that the admin client can't change LN to unsafe connection strings without modify server settings rights
foreach (var forbidden in new string[]
{
"type=clightning;server=tcp://127.0.0.1",
"type=clightning;server=tcp://test",
"type=clightning;server=tcp://test.lan",
"type=clightning;server=tcp://test.local",
"type=clightning;server=tcp://192.168.1.2",
"type=clightning;server=unix://8.8.8.8",
"type=clightning;server=unix://[::1]",
"type=clightning;server=unix://[0:0:0:0:0:0:0:1]",
})
{
var ex = await AssertValidationError(new[] { "ConnectionString" }, async () =>
{
await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData()
{
ConnectionString = forbidden,
CryptoCode = "BTC",
Enabled = true
});
});
Assert.Contains("btcpay.server.canmodifyserversettings", ex.Message);
// However, the other client should work because he has `btcpay.server.canmodifyserversettings`
await admin2Client.UpdateStoreLightningNetworkPaymentMethod(admin2.StoreId, "BTC", new LightningNetworkPaymentMethodData()
{
ConnectionString = forbidden,
CryptoCode = "BTC",
Enabled = true
});
}
// Allowed ip should be ok
await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData()
{
ConnectionString = "type=clightning;server=tcp://8.8.8.8",
CryptoCode = "BTC",
Enabled = true
});
// If we strip the admin's right, he should not be able to set unsafe anymore, even if the API key is still valid
await admin2.MakeAdmin(false);
await AssertValidationError(new[] { "ConnectionString" }, async () =>
{
await admin2Client.UpdateStoreLightningNetworkPaymentMethod(admin2.StoreId, "BTC", new LightningNetworkPaymentMethodData()
{
ConnectionString = "type=clightning;server=tcp://127.0.0.1",
CryptoCode = "BTC",
Enabled = true
});
});
var settings = (await tester.PayTester.GetService<SettingsRepository>().GetSettingAsync<PoliciesSettings>())?? new PoliciesSettings();

View file

@ -7,6 +7,8 @@ using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Tests.Lnd;
using BTCPayServer.Tests.Logging;
using NBitcoin;
@ -86,6 +88,10 @@ namespace BTCPayServer.Tests
#endif
public void ActivateLightning()
{
ActivateLightning(LightningConnectionType.Charge);
}
public void ActivateLightning(LightningConnectionType internalNode)
{
var btc = NetworkProvider.GetNetwork<BTCPayNetwork>("BTC").NBitcoinNetwork;
CustomerLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
@ -93,7 +99,39 @@ namespace BTCPayServer.Tests
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify;allowinsecure=true", "merchant_lightningd", btc);
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "http://lnd:lnd@127.0.0.1:35531/", "merchant_lnd", btc);
PayTester.UseLightning = true;
PayTester.IntegratedLightning = MerchantCharge.Client.Uri;
PayTester.IntegratedLightning = GetLightningConnectionString(internalNode, true);
}
public string GetLightningConnectionString(LightningConnectionType? connectionType, bool isMerchant)
{
string connectionString = null;
if (connectionType is null)
return LightningSupportedPaymentMethod.InternalNode;
if (connectionType == LightningConnectionType.Charge)
{
if (isMerchant)
connectionString = $"type=charge;server={MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true";
else
throw new NotSupportedException();
}
else if (connectionType == LightningConnectionType.CLightning)
{
if (isMerchant)
connectionString = "type=clightning;server=" +
((CLightningClient)MerchantLightningD).Address.AbsoluteUri;
else
connectionString = "type=clightning;server=" +
((CLightningClient)CustomerLightningD).Address.AbsoluteUri;
}
else if (connectionType == LightningConnectionType.LndREST)
{
if (isMerchant)
connectionString = $"type=lnd-rest;server={MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else
throw new NotSupportedException();
}
else
throw new NotSupportedException(connectionType.ToString());
return connectionString;
}
public bool Dockerized

View file

@ -258,40 +258,18 @@ namespace BTCPayServer.Tests
{
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult();
}
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true, string storeId = null)
public Task RegisterLightningNodeAsync(string cryptoCode, bool isMerchant = true, string storeId = null)
{
return RegisterLightningNodeAsync(cryptoCode, null, isMerchant, storeId);
}
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType? connectionType, bool isMerchant = true, string storeId = null)
{
var storeController = this.GetController<StoresController>();
string connectionString = null;
if (connectionType == LightningConnectionType.Charge)
{
if (isMerchant)
connectionString = $"type=charge;server={parent.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true";
else
throw new NotSupportedException();
}
else if (connectionType == LightningConnectionType.CLightning)
{
if (isMerchant)
connectionString = "type=clightning;server=" +
((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
else
connectionString = "type=clightning;server=" +
((CLightningClient)parent.CustomerLightningD).Address.AbsoluteUri;
}
else if (connectionType == LightningConnectionType.LndREST)
{
if (isMerchant)
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else
throw new NotSupportedException();
}
else
throw new NotSupportedException(connectionType.ToString());
string connectionString = parent.GetLightningConnectionString(connectionType, isMerchant);
await storeController.AddLightningNode(storeId ?? StoreId,
new LightningNodeViewModel() {ConnectionString = connectionString, SkipPortTest = true}, "save", "BTC");
new LightningNodeViewModel() { ConnectionString = connectionString, SkipPortTest = true }, "save", "BTC");
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
}

View file

@ -412,7 +412,7 @@ namespace BTCPayServer.Tests
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var entity = new InvoiceEntity();
entity.Networks = networkProvider;
@ -700,7 +700,7 @@ namespace BTCPayServer.Tests
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var entity = new InvoiceEntity();
entity.Networks = networkProvider;
@ -895,7 +895,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
await user.GrantAccessAsync();
await user.GrantAccessAsync(true);
await user.RegisterDerivationSchemeAsync("BTC");
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
user.SetNetworkFeeMode(NetworkFeeMode.Never);
@ -945,7 +945,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.GrantAccess(true);
var storeController = user.GetController<StoresController>();
Assert.IsType<ViewResult>(storeController.UpdateStore());
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC"));
@ -1012,7 +1012,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.GrantAccess(true);
user.RegisterLightningNode("BTC", type);
user.RegisterDerivationScheme("BTC");
@ -2126,7 +2126,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.GrantAccess(true);
user.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit);
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
@ -2184,7 +2184,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.GrantAccess(true);
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
@ -2992,6 +2992,7 @@ namespace BTCPayServer.Tests
var fetcher = new RateFetcher(factory);
var pairs =
provider.GetAll()
.Where(c => c.CryptoCode != "DASH") // ERR_RATE_UNAVAILABLE(bittrex, DASH_BTC)
.Select(c => new CurrencyPair(c.CryptoCode, "USD"))
.ToHashSet();
@ -3410,7 +3411,86 @@ namespace BTCPayServer.Tests
Assert.False(fn.Seen);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDoLightningInternalNodeMigration()
{
using (var tester = ServerTester.Create(newDb: true))
{
tester.ActivateLightning(LightningConnectionType.CLightning);
await tester.StartAsync();
var acc = tester.NewAccount();
await acc.GrantAccessAsync(true);
await acc.CreateStoreAsync();
// Test if legacy DerivationStrategy column is converted to DerivationStrategies
var store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
var xpub = "tpubDDmH1briYfZcTDMEc7uMEA5hinzjUTzR9yMC1drxTMeiWyw1VyCqTuzBke6df2sqbfw9QG6wbgTLF5yLjcXsZNaXvJMZLwNEwyvmiFWcLav";
var derivation = $"{xpub}-[legacy]";
store.DerivationStrategy = derivation;
await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
Assert.True(string.IsNullOrEmpty(store.DerivationStrategy));
var v = (DerivationSchemeSettings)store.GetSupportedPaymentMethods(tester.NetworkProvider).First();
Assert.Equal(derivation, v.AccountDerivation.ToString());
Assert.Equal(derivation, v.AccountOriginal.ToString());
Assert.Equal(xpub, v.SigningKey.ToString());
Assert.Equal(xpub, v.GetSigningAccountKeySettings().AccountKey.ToString());
await acc.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning, true);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
var lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
Assert.NotNull(lnMethod.GetExternalLightningUrl());
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
Assert.Null(lnMethod.GetExternalLightningUrl());
// Test if legacy lightning charge settings are converted to LightningConnectionString
store.DerivationStrategies = new JObject()
{
new JProperty("BTC_LightningLike", new JObject()
{
new JProperty("LightningChargeUrl", "http://mycharge.com/"),
new JProperty("Username", "usr"),
new JProperty("Password", "pass"),
new JProperty("CryptoCode", "BTC"),
new JProperty("PaymentId", "someshit"),
})
}.ToString();
await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
Assert.NotNull(lnMethod.GetExternalLightningUrl());
var url = lnMethod.GetExternalLightningUrl();
Assert.Equal(LightningConnectionType.Charge, url.ConnectionType);
Assert.Equal("pass", url.Password);
Assert.Equal("usr", url.Username);
// Test if lightning connection strings get migrated to internal
store.DerivationStrategies = new JObject()
{
new JProperty("BTC_LightningLike", new JObject()
{
new JProperty("CryptoCode", "BTC"),
new JProperty("LightningConnectionString", tester.PayTester.IntegratedLightning),
})
}.ToString();
await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
Assert.True(lnMethod.IsInternalNode);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDoInvoiceMigrations()
@ -3424,7 +3504,7 @@ namespace BTCPayServer.Tests
await acc.CreateStoreAsync();
await acc.RegisterDerivationSchemeAsync("BTC");
var store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
var blob = store.GetStoreBlob();
var serializer = new Serializer(null);
@ -3445,10 +3525,10 @@ namespace BTCPayServer.Tests
new KeyPath("44'/0'/0'").ToString()
}
})));
blob.AdditionalData.Add("networkFeeDisabled", JToken.Parse(
serializer.ToString((bool?)true)));
blob.AdditionalData.Add("onChainMinValue", JToken.Parse(
serializer.ToString(new CurrencyValue()
{
@ -3461,18 +3541,13 @@ namespace BTCPayServer.Tests
Currency = "USD",
Value = 5m
}.ToString())));
store.SetStoreBlob(blob);
await tester.PayTester.StoreRepository.UpdateStore(store);
var settings = tester.PayTester.GetService<SettingsRepository>();
await settings.UpdateSetting<MigrationSettings>(new MigrationSettings());
var migrationStartupTask = tester.PayTester.GetService<IServiceProvider>().GetServices<IStartupTask>()
.Single(task => task is MigrationStartupTask);
await migrationStartupTask.ExecuteAsync();
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
blob = store.GetStoreBlob();
Assert.Empty(blob.AdditionalData);
Assert.Single(blob.PaymentMethodCriteria);
@ -3487,7 +3562,16 @@ namespace BTCPayServer.Tests
}
}
private static async Task RestartMigration(ServerTester tester)
{
var settings = tester.PayTester.GetService<SettingsRepository>();
await settings.UpdateSetting<MigrationSettings>(new MigrationSettings());
var migrationStartupTask = tester.PayTester.GetService<IServiceProvider>().GetServices<IStartupTask>()
.Single(task => task is MigrationStartupTask);
await migrationStartupTask.ExecuteAsync();
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task EmailSenderTests()

View file

@ -28,8 +28,9 @@ namespace BTCPayServer.Controllers.GreenField
public InternalLightningNodeApiController(
BTCPayNetworkProvider btcPayNetworkProvider, BTCPayServerEnvironment btcPayServerEnvironment,
CssThemeManager cssThemeManager, LightningClientFactoryService lightningClientFactory,
IOptions<LightningNetworkOptions> lightningNetworkOptions ) : base(
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager)
IOptions<LightningNetworkOptions> lightningNetworkOptions,
IAuthorizationService authorizationService) : base(
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager, authorizationService)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_lightningClientFactory = lightningClientFactory;
@ -100,17 +101,17 @@ namespace BTCPayServer.Controllers.GreenField
return base.CreateInvoice(cryptoCode, request);
}
protected override Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings)
protected override async Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings)
{
_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode,
out var internalLightningNode);
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network == null || !CanUseInternalLightning(doingAdminThings) || internalLightningNode == null)
if (network == null ||
!_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(network.CryptoCode,
out var internalLightningNode) ||
!await CanUseInternalLightning(doingAdminThings))
{
return Task.FromResult<ILightningClient>(null);
return null;
}
return Task.FromResult(_lightningClientFactory.Create(internalLightningNode, network));
return _lightningClientFactory.Create(internalLightningNode, network);
}
}
}

View file

@ -31,8 +31,9 @@ namespace BTCPayServer.Controllers.GreenField
public StoreLightningNodeApiController(
IOptions<LightningNetworkOptions> lightningNetworkOptions,
LightningClientFactoryService lightningClientFactory, BTCPayNetworkProvider btcPayNetworkProvider,
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager) : base(
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager)
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager,
IAuthorizationService authorizationService) : base(
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager, authorizationService)
{
_lightningNetworkOptions = lightningNetworkOptions;
_lightningClientFactory = lightningClientFactory;
@ -100,11 +101,10 @@ namespace BTCPayServer.Controllers.GreenField
return base.CreateInvoice(cryptoCode, request);
}
protected override Task<ILightningClient> GetLightningClient(string cryptoCode,
protected override async Task<ILightningClient> GetLightningClient(string cryptoCode,
bool doingAdminThings)
{
_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode,
out var internalLightningNode);
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var store = HttpContext.GetStoreData();
@ -117,13 +117,20 @@ namespace BTCPayServer.Controllers.GreenField
var existing = store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
if (existing == null || (existing.GetLightningUrl().IsInternalNode(internalLightningNode) &&
!CanUseInternalLightning(doingAdminThings)))
if (existing == null)
return null;
if (existing.GetExternalLightningUrl() is LightningConnectionString connectionString)
{
return Task.FromResult<ILightningClient>(null);
return _lightningClientFactory.Create(connectionString, network);
}
return Task.FromResult(_lightningClientFactory.Create(existing.GetLightningUrl(), network));
else if (
await CanUseInternalLightning(doingAdminThings) &&
_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(network.CryptoCode,
out var internalLightningNode))
{
return _lightningClientFactory.Create(internalLightningNode, network);
}
return null;
}
}
}

View file

@ -2,10 +2,13 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json.Linq;
@ -32,13 +35,16 @@ namespace BTCPayServer.Controllers.GreenField
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
private readonly CssThemeManager _cssThemeManager;
private readonly IAuthorizationService _authorizationService;
protected LightningNodeApiController(BTCPayNetworkProvider btcPayNetworkProvider,
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager)
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager,
IAuthorizationService authorizationService)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_btcPayServerEnvironment = btcPayServerEnvironment;
_cssThemeManager = cssThemeManager;
_authorizationService = authorizationService;
}
public virtual async Task<IActionResult> GetInfo(string cryptoCode)
@ -294,10 +300,11 @@ namespace BTCPayServer.Controllers.GreenField
};
}
protected bool CanUseInternalLightning(bool doingAdminThings)
protected async Task<bool> CanUseInternalLightning(bool doingAdminThings)
{
return (_btcPayServerEnvironment.IsDeveloping || User.IsInRole(Roles.ServerAdmin) ||
(_cssThemeManager.AllowLightningInternalNodeForAll && !doingAdminThings));
return (!doingAdminThings && _cssThemeManager.AllowLightningInternalNodeForAll) ||
(await _authorizationService.AuthorizeAsync(User, null,
new PolicyRequirement(Policies.CanUseInternalLightningNode))).Succeeded;
}
protected abstract Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings);

View file

@ -57,7 +57,7 @@ namespace BTCPayServer.Controllers.GreenField
.OfType<LightningSupportedPaymentMethod>()
.Select(paymentMethod =>
new LightningNetworkPaymentMethodData(paymentMethod.PaymentId.CryptoCode,
paymentMethod.GetLightningUrl().ToString(), !excludedPaymentMethods.Match(paymentMethod.PaymentId)))
paymentMethod.GetExternalLightningUrl().ToString(), !excludedPaymentMethods.Match(paymentMethod.PaymentId)))
.Where((result) => !enabledOnly || result.Enabled)
.ToList()
);
@ -110,8 +110,6 @@ namespace BTCPayServer.Controllers.GreenField
return NotFound();
}
var internalLightning = await GetInternalLightningNode(network.CryptoCode);
if (string.IsNullOrEmpty(paymentMethodData?.ConnectionString))
{
ModelState.AddModelError(nameof(LightningNetworkPaymentMethodData.ConnectionString),
@ -124,66 +122,44 @@ namespace BTCPayServer.Controllers.GreenField
LightningSupportedPaymentMethod paymentMethod = null;
if (!string.IsNullOrEmpty(paymentMethodData.ConnectionString))
{
if (!LightningConnectionString.TryParse(paymentMethodData.ConnectionString, false,
out var connectionString, out var error))
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), $"Invalid URL ({error})");
return this.CreateValidationError(ModelState);
}
if (connectionString.ConnectionType == LightningConnectionType.LndGRPC)
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString),
$"BTCPay does not support gRPC connections");
return this.CreateValidationError(ModelState);
}
bool isInternalNode = connectionString.IsInternalNode(internalLightning);
if (connectionString.BaseUri.Scheme == "http")
{
if (!isInternalNode && !connectionString.AllowInsecure)
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), "The url must be HTTPS");
return this.CreateValidationError(ModelState);
}
}
if (connectionString.MacaroonFilePath != null)
if (paymentMethodData.ConnectionString == LightningSupportedPaymentMethod.InternalNode)
{
if (!await CanUseInternalLightning())
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString),
"You are not authorized to use macaroonfilepath");
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), $"You are not authorized to use the internal lightning node");
return this.CreateValidationError(ModelState);
}
if (!System.IO.File.Exists(connectionString.MacaroonFilePath))
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetInternalNode();
}
else
{
if (!LightningConnectionString.TryParse(paymentMethodData.ConnectionString, false,
out var connectionString, out var error))
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), $"Invalid URL ({error})");
return this.CreateValidationError(ModelState);
}
if (connectionString.ConnectionType == LightningConnectionType.LndGRPC)
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString),
"The macaroonfilepath file does not exist");
$"BTCPay does not support gRPC connections");
return this.CreateValidationError(ModelState);
}
if (!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))
if (!await CanManageServer() && !connectionString.IsSafe())
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString),
"The macaroonfilepath should be fully rooted");
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), $"You do not have 'btcpay.server.canmodifyserversettings' rights, so the connection string should not contain 'cookiefilepath', 'macaroondirectorypath', 'macaroonfilepath', and should not point to a local ip or to a dns name ending with '.internal', '.local', '.lan' or '.'.");
return this.CreateValidationError(ModelState);
}
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetLightningUrl(connectionString);
}
if (isInternalNode && !await CanUseInternalLightning())
{
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), "Unauthorized url");
return this.CreateValidationError(ModelState);
}
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetLightningUrl(connectionString);
}
var store = Store;
@ -209,7 +185,7 @@ namespace BTCPayServer.Controllers.GreenField
return paymentMethod == null
? null
: new LightningNetworkPaymentMethodData(paymentMethod.PaymentId.CryptoCode,
paymentMethod.GetLightningUrl().ToString(), !excluded);
paymentMethod.GetDisplayableConnectionString(), !excluded);
}
private bool GetNetwork(string cryptoCode, out BTCPayNetwork network)
@ -219,20 +195,17 @@ namespace BTCPayServer.Controllers.GreenField
return network != null;
}
private async Task<LightningConnectionString> GetInternalLightningNode(string cryptoCode)
{
if (_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var connectionString))
{
return await CanUseInternalLightning() ? connectionString : null;
}
return null;
}
private async Task<bool> CanUseInternalLightning()
{
return _cssThemeManager.AllowLightningInternalNodeForAll ||
(await _authorizationService.AuthorizeAsync(User, null,
new PolicyRequirement(Policies.CanUseInternalLightningNode))).Succeeded;
}
private async Task<bool> CanManageServer()
{
return
(await _authorizationService.AuthorizeAsync(User, null,
new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded;
}
}
}

View file

@ -25,7 +25,6 @@ namespace BTCPayServer.Controllers
LightningNodeViewModel vm = new LightningNodeViewModel
{
CryptoCode = cryptoCode,
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString(),
StoreId = storeId
};
SetExistingValues(store, vm);
@ -34,8 +33,12 @@ namespace BTCPayServer.Controllers
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
{
vm.ConnectionString = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
if (GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store) is LightningSupportedPaymentMethod paymentMethod)
{
vm.ConnectionString = paymentMethod.GetDisplayableConnectionString();
}
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.LightningLike));
vm.CanUseInternalNode = CanUseInternalLightning();
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
{
@ -45,16 +48,6 @@ namespace BTCPayServer.Controllers
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
private LightningConnectionString GetInternalLighningNode(string cryptoCode)
{
if (_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var connectionString))
{
return CanUseInternalLightning() ? connectionString : null;
}
return null;
}
[HttpPost]
[Route("{storeId}/lightning/{cryptoCode}")]
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
@ -65,8 +58,6 @@ namespace BTCPayServer.Controllers
return NotFound();
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
var internalLightning = GetInternalLighningNode(network.CryptoCode);
vm.InternalLightningNode = internalLightning?.ToString();
if (network == null)
{
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
@ -75,7 +66,20 @@ namespace BTCPayServer.Controllers
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
if (!string.IsNullOrEmpty(vm.ConnectionString))
if (vm.ConnectionString == LightningSupportedPaymentMethod.InternalNode)
{
if (!CanUseInternalLightning())
{
ModelState.AddModelError(nameof(vm.ConnectionString), $"You are not authorized to use the internal lightning node");
return View(vm);
}
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetInternalNode();
}
else if (!string.IsNullOrEmpty(vm.ConnectionString))
{
if (!LightningConnectionString.TryParse(vm.ConnectionString, false, out var connectionString, out var error))
{
@ -88,40 +92,9 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.ConnectionString), $"BTCPay does not support gRPC connections");
return View(vm);
}
bool isInternalNode = connectionString.IsInternalNode(internalLightning);
if (connectionString.BaseUri.Scheme == "http")
if (!User.IsInRole(Roles.ServerAdmin) && !connectionString.IsSafe())
{
if (!isInternalNode && !connectionString.AllowInsecure)
{
ModelState.AddModelError(nameof(vm.ConnectionString), "The url must be HTTPS");
return View(vm);
}
}
if (connectionString.MacaroonFilePath != null)
{
if (!CanUseInternalLightning())
{
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use macaroonfilepath");
return View(vm);
}
if (!System.IO.File.Exists(connectionString.MacaroonFilePath))
{
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does not exist");
return View(vm);
}
if (!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))
{
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath should be fully rooted");
return View(vm);
}
}
if (isInternalNode && !CanUseInternalLightning())
{
ModelState.AddModelError(nameof(vm.ConnectionString), "Unauthorized url");
ModelState.AddModelError(nameof(vm.ConnectionString), $"You are not a server admin, so the connection string should not contain 'cookiefilepath', 'macaroondirectorypath', 'macaroonfilepath', and should not point to a local ip or to a dns name ending with '.internal', '.local', '.lan' or '.'.");
return View(vm);
}
@ -170,10 +143,9 @@ namespace BTCPayServer.Controllers
return View(vm);
}
}
private bool CanUseInternalLightning()
{
return (_BTCPayEnv.IsDeveloping || User.IsInRole(Roles.ServerAdmin) || _CssThemeManager.AllowLightningInternalNodeForAll);
return User.IsInRole(Roles.ServerAdmin) || _CssThemeManager.AllowLightningInternalNodeForAll;
}
}
}

View file

@ -546,8 +546,8 @@ namespace BTCPayServer.Controllers
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
{
CryptoCode = paymentMethodId.CryptoCode,
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty,
Enabled = !excludeFilters.Match(paymentMethodId) && lightning?.GetLightningUrl() != null
Address = lightning?.GetExternalLightningUrl()?.BaseUri.AbsoluteUri ?? "Internal node",
Enabled = !excludeFilters.Match(paymentMethodId)
});
break;
}

View file

@ -69,14 +69,6 @@ namespace BTCPayServer.Data
#pragma warning disable CS0618
bool btcReturned = false;
// Legacy stuff which should go away
if (!string.IsNullOrEmpty(storeData.DerivationStrategy))
{
btcReturned = true;
yield return DerivationSchemeSettings.Parse(storeData.DerivationStrategy, networks.BTC);
}
if (!string.IsNullOrEmpty(storeData.DerivationStrategies))
{
JObject strategies = JObject.Parse(storeData.DerivationStrategies);
@ -130,11 +122,6 @@ namespace BTCPayServer.Data
foreach (var strat in strategies.Properties().ToList())
{
var stratId = PaymentMethodId.Parse(strat.Name);
if (stratId.IsBTCOnChain)
{
// Legacy stuff which should go away
storeData.DerivationStrategy = null;
}
if (stratId == paymentMethodId)
{
if (supportedPaymentMethod == null)
@ -149,12 +136,7 @@ namespace BTCPayServer.Data
break;
}
}
if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
{
storeData.DerivationStrategy = null;
}
else if (!existing && supportedPaymentMethod != null)
if (!existing && supportedPaymentMethod != null)
strategies.Add(new JProperty(supportedPaymentMethod.PaymentId.ToString(), PaymentMethodExtensions.Serialize(supportedPaymentMethod)));
storeData.DerivationStrategies = strategies.ToString();
#pragma warning restore CS0618

View file

@ -45,13 +45,20 @@ namespace BTCPayServer
endpoint = bip21.UnknowParameters.TryGetValue($"{PayjoinClient.BIP21EndpointKey}", out var uri) ? new Uri(uri, UriKind.Absolute) : null;
return endpoint != null;
}
public static bool IsInternalNode(this LightningConnectionString connectionString, LightningConnectionString internalLightning)
{
var internalDomain = internalLightning?.BaseUri?.DnsSafeHost;
return connectionString.ConnectionType == LightningConnectionType.CLightning ||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
(internalDomain == "127.0.0.1" || internalDomain == "localhost");
public static bool IsSafe(this LightningConnectionString connectionString)
{
if (connectionString.CookieFilePath != null ||
connectionString.MacaroonDirectoryPath != null ||
connectionString.MacaroonFilePath != null)
return false;
var uri = connectionString.BaseUri;
if (uri.Scheme.Equals("unix", StringComparison.OrdinalIgnoreCase))
return false;
if (!NBitcoin.Utils.TryParseEndpoint(uri.DnsSafeHost, 80, out var endpoint))
return false;
return !Extensions.IsLocalNetwork(uri.DnsSafeHost);
}
public static IQueryable<TEntity> Where<TEntity>(this Microsoft.EntityFrameworkCore.DbSet<TEntity> obj, System.Linq.Expressions.Expression<Func<TEntity, bool>> predicate) where TEntity : class

View file

@ -11,14 +11,17 @@ using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NBitcoin.DataEncoders;
using NBXplorer;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Hosting
{
@ -29,11 +32,15 @@ namespace BTCPayServer.Hosting
private readonly BTCPayNetworkProvider _NetworkProvider;
private readonly SettingsRepository _Settings;
private readonly UserManager<ApplicationUser> _userManager;
public IOptions<LightningNetworkOptions> LightningOptions { get; }
public MigrationStartupTask(
BTCPayNetworkProvider networkProvider,
StoreRepository storeRepository,
ApplicationDbContextFactory dbContextFactory,
UserManager<ApplicationUser> userManager,
IOptions<LightningNetworkOptions> lightningOptions,
SettingsRepository settingsRepository)
{
_DBContextFactory = dbContextFactory;
@ -41,6 +48,7 @@ namespace BTCPayServer.Hosting
_NetworkProvider = networkProvider;
_Settings = settingsRepository;
_userManager = userManager;
LightningOptions = lightningOptions;
}
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
{
@ -106,6 +114,13 @@ namespace BTCPayServer.Hosting
settings.TransitionToStoreBlobAdditionalData = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.TransitionInternalNodeConnectionString)
{
await TransitionInternalNodeConnectionString();
settings.TransitionInternalNodeConnectionString = true;
await _Settings.UpdateSetting(settings);
}
}
catch (Exception ex)
{
@ -114,6 +129,100 @@ namespace BTCPayServer.Hosting
}
}
private async Task TransitionInternalNodeConnectionString()
{
var nodes = LightningOptions.Value.InternalLightningByCryptoCode.Values.Select(c => c.ToString()).ToHashSet();
await using var ctx = _DBContextFactory.CreateContext();
foreach (var store in await ctx.Stores.AsQueryable().ToArrayAsync())
{
#pragma warning disable CS0618 // Type or member is obsolete
if (!string.IsNullOrEmpty(store.DerivationStrategy))
{
var noLabel = store.DerivationStrategy.Split('-')[0];
JObject jObject = new JObject();
jObject.Add("BTC", new JObject()
{
new JProperty("signingKey", noLabel),
new JProperty("accountDerivation", store.DerivationStrategy),
new JProperty("accountOriginal", store.DerivationStrategy),
new JProperty("accountKeySettings", new JArray()
{
new JObject()
{
new JProperty("accountKey", noLabel)
}
})
});
store.DerivationStrategies = jObject.ToString();
store.DerivationStrategy = null;
}
if (string.IsNullOrEmpty(store.DerivationStrategies))
continue;
var strats = JObject.Parse(store.DerivationStrategies);
bool updated = false;
foreach (var prop in strats.Properties().Where(p => p.Name.EndsWith("LightningLike", StringComparison.OrdinalIgnoreCase)))
{
var method = ((JObject)prop.Value);
var lightningCharge = method.Property("LightningChargeUrl", StringComparison.OrdinalIgnoreCase);
var ln = method.Property("LightningConnectionString", StringComparison.OrdinalIgnoreCase);
if (lightningCharge != null)
{
var chargeUrl = lightningCharge.Value.Value<string>();
var usr = method.Property("Username", StringComparison.OrdinalIgnoreCase)?.Value.Value<string>();
var pass = method.Property("Password", StringComparison.OrdinalIgnoreCase)?.Value.Value<string>();
updated = true;
if (chargeUrl != null)
{
var fullUri = new UriBuilder(chargeUrl)
{
UserName = usr,
Password = pass
}.Uri.AbsoluteUri;
var newStr = $"type=charge;server={fullUri};allowinsecure=true";
if (ln is null)
{
ln = new JProperty("LightningConnectionString", newStr);
method.Add(ln);
}
else
{
ln.Value = newStr;
}
}
foreach (var p in new[] { "Username", "Password", "LightningChargeUrl" })
method.Property(p, StringComparison.OrdinalIgnoreCase)?.Remove();
}
var paymentId = method.Property("PaymentId", StringComparison.OrdinalIgnoreCase);
if (paymentId != null)
{
paymentId.Remove();
updated = true;
}
if (ln is null)
continue;
if (nodes.Contains(ln.Value.Value<string>()))
{
updated = true;
ln.Value = null;
if (!(method.Property("InternalNodeRef", StringComparison.OrdinalIgnoreCase) is JProperty internalNode))
{
internalNode = new JProperty("InternalNodeRef", null);
method.Add(internalNode);
}
internalNode.Value = new JValue(LightningSupportedPaymentMethod.InternalNode);
}
}
if (updated)
store.DerivationStrategies = strats.ToString();
#pragma warning restore CS0618 // Type or member is obsolete
}
await ctx.SaveChangesAsync();
}
private async Task TransitionToStoreBlobAdditionalData()
{
await using var ctx = _DBContextFactory.CreateContext();
@ -342,8 +451,8 @@ retry:
{
foreach (var method in store.GetSupportedPaymentMethods(_NetworkProvider).OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
{
var lightning = method.GetLightningUrl();
if (lightning.IsLegacy)
var lightning = method.GetExternalLightningUrl();
if (lightning?.IsLegacy is true)
{
method.SetLightningUrl(lightning);
store.SetSupportedPaymentMethod(method);

View file

@ -16,7 +16,8 @@ namespace BTCPayServer.Models.StoreViewModels
get;
set;
}
public string InternalLightningNode { get; internal set; }
public bool CanUseInternalNode { get; set; }
public bool SkipPortTest { get; set; }
[Display(Name="Lightning enabled")]

View file

@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
@ -14,6 +15,7 @@ using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Options;
using NBitcoin;
namespace BTCPayServer.Payments.Lightning
@ -32,16 +34,21 @@ namespace BTCPayServer.Payments.Lightning
LightningClientFactoryService lightningClientFactory,
BTCPayNetworkProvider networkProvider,
SocketFactory socketFactory,
CurrencyNameTable currencyNameTable)
CurrencyNameTable currencyNameTable,
IOptions<LightningNetworkOptions> options)
{
_Dashboard = dashboard;
_lightningClientFactory = lightningClientFactory;
_networkProvider = networkProvider;
_socketFactory = socketFactory;
_currencyNameTable = currencyNameTable;
Options = options;
}
public override PaymentType PaymentType => PaymentTypes.LightningLike;
public IOptions<LightningNetworkOptions> Options { get; }
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(
InvoiceLogs logs,
LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store,
@ -61,7 +68,7 @@ namespace BTCPayServer.Payments.Lightning
{
// ignored
}
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
var client = CreateLightningClient(supportedPaymentMethod, network);
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
if (expiry < TimeSpan.Zero)
expiry = TimeSpan.FromSeconds(1);
@ -105,7 +112,7 @@ namespace BTCPayServer.Payments.Lightning
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
{
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
var client = CreateLightningClient(supportedPaymentMethod, network);
LightningNodeInformation info;
try
{
@ -135,6 +142,21 @@ namespace BTCPayServer.Payments.Lightning
}
}
private ILightningClient CreateLightningClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
var external = supportedPaymentMethod.GetExternalLightningUrl();
if (external != null)
{
return _lightningClientFactory.Create(external, network);
}
else
{
if (!Options.Value.InternalLightningByCryptoCode.TryGetValue(network.CryptoCode, out var connectionString))
throw new PaymentMethodUnavailableException("No internal node configured");
return _lightningClientFactory.Create(connectionString, network);
}
}
public async Task TestConnection(NodeInfo nodeInfo, CancellationToken cancellation)
{
try

View file

@ -7,6 +7,7 @@ using System.Threading.Channels;
using System.Threading.Tasks;
using AngleSharp.Dom.Events;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Lightning;
@ -17,6 +18,7 @@ using BTCPayServer.Services.Stores;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NBXplorer;
namespace BTCPayServer.Payments.Lightning
@ -40,7 +42,8 @@ namespace BTCPayServer.Payments.Lightning
BTCPayNetworkProvider networkProvider,
LightningClientFactoryService lightningClientFactory,
LightningLikePaymentHandler lightningLikePaymentHandler,
StoreRepository storeRepository)
StoreRepository storeRepository,
IOptions<LightningNetworkOptions> options)
{
_Aggregator = aggregator;
_InvoiceRepository = invoiceRepository;
@ -49,6 +52,7 @@ namespace BTCPayServer.Payments.Lightning
this.lightningClientFactory = lightningClientFactory;
_lightningLikePaymentHandler = lightningLikePaymentHandler;
_storeRepository = storeRepository;
Options = options;
}
async Task CheckingInvoice(CancellationToken cancellation)
@ -60,11 +64,11 @@ namespace BTCPayServer.Payments.Lightning
{
foreach (var listenedInvoice in (await GetListenedInvoices(invoiceId)).Where(i => !i.IsExpired()))
{
var instanceListenerKey = (listenedInvoice.Network.CryptoCode, listenedInvoice.SupportedPaymentMethod.GetLightningUrl().ToString());
var instanceListenerKey = (listenedInvoice.Network.CryptoCode, GetLightningUrl(listenedInvoice.SupportedPaymentMethod).ToString());
if (!_InstanceListeners.TryGetValue(instanceListenerKey, out var instanceListener) ||
!instanceListener.IsListening)
{
instanceListener ??= new LightningInstanceListener(_InvoiceRepository, _Aggregator, listenedInvoice.SupportedPaymentMethod, lightningClientFactory, listenedInvoice.Network);
instanceListener ??= new LightningInstanceListener(_InvoiceRepository, _Aggregator, lightningClientFactory, listenedInvoice.Network, GetLightningUrl(listenedInvoice.SupportedPaymentMethod));
var status = await instanceListener.PollPayment(listenedInvoice, cancellation);
if (status is null ||
status is LightningInvoiceStatus.Paid ||
@ -119,7 +123,7 @@ namespace BTCPayServer.Payments.Lightning
listenedInvoices.Add(new ListenedInvoice()
{
Expiration = invoice.ExpirationTime,
Uri = lightningSupportedMethod.GetLightningUrl().BaseUri.AbsoluteUri,
Uri = GetLightningUrl(lightningSupportedMethod).BaseUri.AbsoluteUri,
PaymentMethodDetails = lightningMethod,
SupportedPaymentMethod = lightningSupportedMethod,
PaymentMethod = paymentMethod,
@ -206,7 +210,7 @@ namespace BTCPayServer.Payments.Lightning
paymentMethod.Network, prepObj));
var instanceListenerKey = (paymentMethod.Network.CryptoCode,
supportedMethod.GetLightningUrl().ToString());
GetLightningUrl(supportedMethod).ToString());
if (_InstanceListeners.TryGetValue(instanceListenerKey, out var instanceListener))
{
await _InvoiceRepository.NewPaymentDetails(invoice.Id, newPaymentMethodDetails,
@ -215,7 +219,7 @@ namespace BTCPayServer.Payments.Lightning
instanceListener.AddListenedInvoice(new ListenedInvoice()
{
Expiration = invoice.ExpirationTime,
Uri = supportedMethod.GetLightningUrl().BaseUri.AbsoluteUri,
Uri = GetLightningUrl(supportedMethod).BaseUri.AbsoluteUri,
PaymentMethodDetails = newPaymentMethodDetails,
SupportedPaymentMethod = supportedMethod,
PaymentMethod = paymentMethod,
@ -240,6 +244,16 @@ namespace BTCPayServer.Payments.Lightning
}
private LightningConnectionString GetLightningUrl(LightningSupportedPaymentMethod supportedMethod)
{
var url = supportedMethod.GetExternalLightningUrl();
if (url != null)
return url;
if (Options.Value.InternalLightningByCryptoCode.TryGetValue(supportedMethod.CryptoCode, out var conn))
return conn;
throw new InvalidOperationException($"{supportedMethod.CryptoCode}: The internal lightning node is not set up");
}
TimeSpan _PollInterval = TimeSpan.FromMinutes(1.0);
public TimeSpan PollInterval
{
@ -257,6 +271,8 @@ namespace BTCPayServer.Payments.Lightning
}
}
public IOptions<LightningNetworkOptions> Options { get; }
readonly CancellationTokenSource _Cts = new CancellationTokenSource();
private Timer _ListenPoller;
@ -287,23 +303,26 @@ namespace BTCPayServer.Payments.Lightning
public class LightningInstanceListener
{
private readonly LightningSupportedPaymentMethod supportedPaymentMethod;
private readonly InvoiceRepository invoiceRepository;
private readonly EventAggregator _eventAggregator;
private readonly BTCPayNetwork network;
private readonly LightningClientFactoryService _lightningClientFactory;
public LightningConnectionString ConnectionString { get; }
public LightningInstanceListener(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
LightningSupportedPaymentMethod supportedPaymentMethod,
LightningClientFactoryService lightningClientFactory,
BTCPayNetwork network)
BTCPayNetwork network,
LightningConnectionString connectionString)
{
this.supportedPaymentMethod = supportedPaymentMethod;
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
this.invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
this.network = network;
_lightningClientFactory = lightningClientFactory;
ConnectionString = connectionString;
}
internal bool AddListenedInvoice(ListenedInvoice invoice)
{
@ -312,12 +331,12 @@ namespace BTCPayServer.Payments.Lightning
internal async Task<LightningInvoiceStatus?> PollPayment(ListenedInvoice listenedInvoice, CancellationToken cancellation)
{
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
var client = _lightningClientFactory.Create(ConnectionString, network);
LightningInvoice lightningInvoice = await client.GetInvoice(listenedInvoice.PaymentMethodDetails.InvoiceId);
if (lightningInvoice?.Status is LightningInvoiceStatus.Paid &&
await AddPayment(lightningInvoice, listenedInvoice.InvoiceId))
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Payment detected via polling on {listenedInvoice.InvoiceId}");
Logs.PayServer.LogInformation($"{network.CryptoCode} (Lightning): Payment detected via polling on {listenedInvoice.InvoiceId}");
}
return lightningInvoice?.Status;
}
@ -335,17 +354,17 @@ namespace BTCPayServer.Payments.Lightning
public CancellationTokenSource StopListeningCancellationTokenSource;
async Task Listen(CancellationToken cancellation)
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
Logs.PayServer.LogInformation($"{network.CryptoCode} (Lightning): Start listening {ConnectionString.BaseUri}");
try
{
var lightningClient = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
var lightningClient = _lightningClientFactory.Create(ConnectionString, network);
using (var session = await lightningClient.Listen(cancellation))
{
// Just in case the payment arrived after our last poll but before we listened.
await PollAllListenedInvoices(cancellation);
if (_ErrorAlreadyLogged)
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Could reconnect successfully to {supportedPaymentMethod.GetLightningUrl().BaseUri}");
Logs.PayServer.LogInformation($"{network.CryptoCode} (Lightning): Could reconnect successfully to {ConnectionString.BaseUri}");
}
_ErrorAlreadyLogged = false;
while (!_ListenedInvoices.IsEmpty)
@ -361,7 +380,7 @@ namespace BTCPayServer.Payments.Lightning
{
if (await AddPayment(notification, listenedInvoice.InvoiceId))
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Payment detected via notification ({listenedInvoice.InvoiceId})");
Logs.PayServer.LogInformation($"{network.CryptoCode} (Lightning): Payment detected via notification ({listenedInvoice.InvoiceId})");
}
_ListenedInvoices.TryRemove(notification.Id, out var _);
}
@ -376,12 +395,12 @@ namespace BTCPayServer.Payments.Lightning
catch (Exception ex) when (!cancellation.IsCancellationRequested && !_ErrorAlreadyLogged)
{
_ErrorAlreadyLogged = true;
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningUrl().BaseUri}");
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
Logs.PayServer.LogError(ex, $"{network.CryptoCode} (Lightning): Error while contacting {ConnectionString.BaseUri}");
Logs.PayServer.LogInformation($"{network.CryptoCode} (Lightning): Stop listening {ConnectionString.BaseUri}");
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { }
if (_ListenedInvoices.IsEmpty)
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): No more invoice to listen on {supportedPaymentMethod.GetLightningUrl().BaseUri}, releasing the connection.");
Logs.PayServer.LogInformation($"{network.CryptoCode} (Lightning): No more invoice to listen on {ConnectionString.BaseUri}, releasing the connection.");
}
public DateTimeOffset? LastFullPoll { get; set; }

View file

@ -1,27 +1,24 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Lightning;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Payments.Lightning
{
public class LightningSupportedPaymentMethod : ISupportedPaymentMethod
{
public const string InternalNode = "Internal Node";
public string CryptoCode { get; set; }
[Obsolete("Use Get/SetLightningUrl")]
public string Username { get; set; }
[Obsolete("Use Get/SetLightningUrl")]
public string Password { get; set; }
// This property MUST be after CryptoCode or else JSON serialization fails
[JsonIgnore]
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
[Obsolete("Use Get/SetLightningUrl")]
public string LightningChargeUrl { get; set; }
[Obsolete("Use Get/SetLightningUrl")]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string LightningConnectionString { get; set; }
public LightningConnectionString GetLightningUrl()
public LightningConnectionString GetExternalLightningUrl()
{
#pragma warning disable CS0618 // Type or member is obsolete
if (!string.IsNullOrEmpty(LightningConnectionString))
@ -33,14 +30,7 @@ namespace BTCPayServer.Payments.Lightning
return connectionString;
}
else
{
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
if (!BTCPayServer.Lightning.LightningConnectionString.TryParse(fullUri, true, out var connectionString, out var error))
{
throw new FormatException(error);
}
return connectionString;
}
return null;
#pragma warning restore CS0618 // Type or member is obsolete
}
@ -48,13 +38,42 @@ namespace BTCPayServer.Payments.Lightning
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
#pragma warning disable CS0618 // Type or member is obsolete
LightningConnectionString = connectionString.ToString();
Username = null;
Password = null;
LightningChargeUrl = null;
#pragma warning restore CS0618 // Type or member is obsolete
}
public string GetDisplayableConnectionString()
{
#pragma warning disable CS0618 // Type or member is obsolete
if (!string.IsNullOrEmpty(LightningConnectionString) &&
BTCPayServer.Lightning.LightningConnectionString.TryParse(LightningConnectionString, false, out var conn))
return conn.ToString();
#pragma warning restore CS0618 // Type or member is obsolete
if (InternalNodeRef is string s)
return s;
return "Invalid connection string";
}
public void SetInternalNode()
{
#pragma warning disable CS0618 // Type or member is obsolete
LightningConnectionString = null;
InternalNodeRef = InternalNode;
#pragma warning restore CS0618 // Type or member is obsolete
}
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string InternalNodeRef { get; set; }
[JsonIgnore]
public bool IsInternalNode
{
get
{
#pragma warning disable CS0618 // Type or member is obsolete
return InternalNodeRef == InternalNode;
#pragma warning restore CS0618 // Type or member is obsolete
}
}
}
}

View file

@ -58,7 +58,7 @@ namespace BTCPayServer.Services
{
get
{
return DevelopmentOverride?? NetworkType == ChainName.Regtest && Environment.IsDevelopment();
return NetworkType == ChainName.Regtest && Environment.IsDevelopment();
}
}
@ -87,7 +87,5 @@ namespace BTCPayServer.Services
}
return txt.ToString();
}
public bool? DevelopmentOverride;
}
}

View file

@ -11,6 +11,7 @@ namespace BTCPayServer.Services
public bool CheckedFirstRun { get; set; }
public bool PaymentMethodCriteria { get; set; }
public bool TransitionToStoreBlobAdditionalData { get; set; }
public bool TransitionInternalNodeConnectionString { get; set; }
public override string ToString()
{

View file

@ -1,4 +1,4 @@
@model LightningNodeViewModel
@model LightningNodeViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.Index, "Add lightning node");
@ -42,6 +42,14 @@
<div class="form-group">
<p class="mb-2">The connection string encapsulates the configuration for connecting to your lightning node. BTCPay Server currently supports:</p>
<ul>
<li class="mb-2">
<strong>Internal node</strong>, if you are administrator of the server:
<ul>
<li>
<code>Internal Node</code>
</li>
</ul>
</li>
<li class="mb-2">
<strong>c-lightning</strong> via TCP or unix domain socket connection:
<ul>
@ -78,9 +86,6 @@
<li>
<code><b>type=</b>lnd-rest;<b>server=</b>https://mylnd:8080/;<b>macaroon=</b>abef263adfe...;<b>certthumbprint=</b>abef263adfe...</code>
</li>
<li>
<code><b>type=</b>lnd-rest;<b>server=</b>http://mylnd:8080/;<b>macaroonfilepath=</b>/root/.lnd/admin.macaroon;<b>allowinsecure=</b>true</code>
</li>
</ul>
<a class="d-inline-block my-2 text-secondary text-decoration-none" data-toggle="collapse" href="#lnd-notes" role="button" aria-expanded="false" aria-controls="lnd-notes">
<span class="fa fa-question-circle-o" title="More information..."></span> More information on the LND settings
@ -103,29 +108,28 @@
<div class="form-group">
<label asp-for="ConnectionString"></label>
<input asp-for="ConnectionString" class="form-control"/>
<input asp-for="ConnectionString" class="form-control" />
<span asp-validation-for="ConnectionString" class="text-danger"></span>
@if (Model.InternalLightningNode != null)
@if (Model.CanUseInternalNode)
{
<p class="form-text text-muted">
Use the internal lightning node of this BTCPay Server instance by
<a href="#" id="internal-ln-node-setter" onclick="$('#ConnectionString').val('@Model.InternalLightningNode');return false;">clicking here</a>.
<a href="#" id="internal-ln-node-setter" onclick="$('#ConnectionString').val('Internal Node');return false;">clicking here</a>.
</p>
}
</div>
<div class="form-group form-check">
<input asp-for="Enabled" type="checkbox" class="form-check-input"/>
<input asp-for="Enabled" type="checkbox" class="form-check-input" />
<label asp-for="Enabled" class="form-check-label"></label>
</div>
<button id="save" name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
<button name="command" type="submit" value="test" class="btn btn-secondary mr-3">Test connection</button>
<a
class="text-secondary"
asp-controller="PublicLightningNodeInfo"
asp-action="ShowLightningNodeInfo"
asp-route-cryptoCode="@Model.CryptoCode"
asp-route-storeId="@Model.StoreId"
target="_blank">
<a class="text-secondary"
asp-controller="PublicLightningNodeInfo"
asp-action="ShowLightningNodeInfo"
asp-route-cryptoCode="@Model.CryptoCode"
asp-route-storeId="@Model.StoreId"
target="_blank">
<span class="fa fa-info-circle" title="More information..."></span>
Open Public Node Info Page
</a>

View file

@ -241,7 +241,7 @@
},
"connectionString": {
"type": "string",
"description": "The lightning connection string",
"description": "The lightning connection string. Set to 'Internal Node' to use the internal node. (See [this doc](https://github.com/btcpayserver/BTCPayServer.Lightning/blob/master/README.md#examples) for some example)",
"example": "type=clightning;server=..."
}
}