diff --git a/BTCPayServer.Tests/Lnd/UnitTests.cs b/BTCPayServer.Tests/Lnd/UnitTests.cs index b932e4f21..2f1e6789c 100644 --- a/BTCPayServer.Tests/Lnd/UnitTests.cs +++ b/BTCPayServer.Tests/Lnd/UnitTests.cs @@ -59,6 +59,14 @@ namespace BTCPayServer.Tests.Lnd Assert.Equal(createInvoice.BOLT11, getInvoice.BOLT11); } + [Fact] + public void Play() + { + var seq = new System.Buffers.ReadOnlySequence(new ReadOnlyMemory(new byte[1000])); + var seq2 = seq.Slice(3); + var pos = seq2.GetPosition(0); + } + // integration tests [Fact] public async Task TestWaitListenInvoice() diff --git a/BTCPayServer/Controllers/StoresController.LightningLike.cs b/BTCPayServer/Controllers/StoresController.LightningLike.cs index 68df0e884..efb9dc4f1 100644 --- a/BTCPayServer/Controllers/StoresController.LightningLike.cs +++ b/BTCPayServer/Controllers/StoresController.LightningLike.cs @@ -97,6 +97,20 @@ namespace BTCPayServer.Controllers } } + 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 exist"); + return View(vm); + } + } + if (isInternalNode && !CanUseInternalLightning()) { ModelState.AddModelError(nameof(vm.ConnectionString), "Unauthorized url"); diff --git a/BTCPayServer/Payments/Lightning/LightningClientFactory.cs b/BTCPayServer/Payments/Lightning/LightningClientFactory.cs index eb17a4110..1b0e8d5eb 100644 --- a/BTCPayServer/Payments/Lightning/LightningClientFactory.cs +++ b/BTCPayServer/Payments/Lightning/LightningClientFactory.cs @@ -32,6 +32,7 @@ namespace BTCPayServer.Payments.Lightning return new LndInvoiceClient(new LndSwaggerClient(new LndRestSettings(connString.BaseUri) { Macaroon = connString.Macaroon, + MacaroonFilePath = connString.MacaroonFilePath, CertificateThumbprint = connString.CertificateThumbprint, AllowInsecure = connString.AllowInsecure, })); diff --git a/BTCPayServer/Payments/Lightning/LightningConnectionString.cs b/BTCPayServer/Payments/Lightning/LightningConnectionString.cs index e56517e1f..9f245b0c3 100644 --- a/BTCPayServer/Payments/Lightning/LightningConnectionString.cs +++ b/BTCPayServer/Payments/Lightning/LightningConnectionString.cs @@ -196,6 +196,22 @@ namespace BTCPayServer.Payments.Lightning } } + var macaroonFilePath = Take(keyValues, "macaroonfilepath"); + if (macaroonFilePath != null) + { + if(macaroon != null) + { + error = $"The key 'macaroon' is already specified"; + return false; + } + if(!macaroonFilePath.EndsWith(".macaroon", StringComparison.OrdinalIgnoreCase)) + { + error = $"The key 'macaroonfilepath' should point to a .macaroon file"; + return false; + } + result.MacaroonFilePath = macaroonFilePath; + } + string securitySet = null; var certthumbprint = Take(keyValues, "certthumbprint"); if (certthumbprint != null) @@ -223,12 +239,12 @@ namespace BTCPayServer.Payments.Lightning if (allowinsecureStr != null) { var allowedValues = new[] { "true", "false" }; - if(!allowedValues.Any(v=> v.Equals(allowinsecureStr, StringComparison.OrdinalIgnoreCase))) + if (!allowedValues.Any(v => v.Equals(allowinsecureStr, StringComparison.OrdinalIgnoreCase))) { error = $"The key 'allowinsecure' should be true or false"; return false; } - + bool allowInsecure = allowinsecureStr.Equals("true", StringComparison.OrdinalIgnoreCase); if (securitySet != null && allowInsecure) { @@ -238,7 +254,7 @@ namespace BTCPayServer.Payments.Lightning result.AllowInsecure = allowInsecure; } - if(!result.AllowInsecure && result.BaseUri.Scheme == "http") + if (!result.AllowInsecure && result.BaseUri.Scheme == "http") { error = $"The key 'allowinsecure' is false, but server's Uri is not using https"; return false; @@ -340,6 +356,7 @@ namespace BTCPayServer.Payments.Lightning private set; } public byte[] Macaroon { get; set; } + public string MacaroonFilePath { get; set; } public byte[] CertificateThumbprint { get; set; } public bool AllowInsecure { get; set; } @@ -388,11 +405,15 @@ namespace BTCPayServer.Payments.Lightning { builder.Append($";macaroon={Encoder.EncodeData(Macaroon)}"); } + if (MacaroonFilePath != null) + { + builder.Append($";macaroonfilepath={MacaroonFilePath}"); + } if (CertificateThumbprint != null) { builder.Append($";certthumbprint={Encoders.Hex.EncodeData(CertificateThumbprint)}"); } - if(AllowInsecure) + if (AllowInsecure) { builder.Append($";allowinsecure=true"); } diff --git a/BTCPayServer/Payments/Lightning/Lnd/LndAuthentication.cs b/BTCPayServer/Payments/Lightning/Lnd/LndAuthentication.cs new file mode 100644 index 000000000..5f4c4cc93 --- /dev/null +++ b/BTCPayServer/Payments/Lightning/Lnd/LndAuthentication.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using NBitcoin.DataEncoders; + +namespace BTCPayServer.Payments.Lightning.Lnd +{ + public abstract class LndAuthentication + { + public class FixedMacaroonAuthentication : LndAuthentication + { + public FixedMacaroonAuthentication(byte[] macaroon) + { + if (macaroon == null) + throw new ArgumentNullException(nameof(macaroon)); + Macaroon = macaroon; + } + public byte[] Macaroon { get; set; } + public override void AddAuthentication(HttpRequestMessage httpRequest) + { + httpRequest.Headers.Add("Grpc-Metadata-macaroon", Encoders.Hex.EncodeData(Macaroon)); + } + } + public class NullAuthentication : LndAuthentication + { + public static NullAuthentication Instance { get; } = new NullAuthentication(); + + private NullAuthentication() + { + + } + public override void AddAuthentication(HttpRequestMessage httpRequest) + { + } + } + + public class MacaroonFileAuthentication : LndAuthentication + { + public MacaroonFileAuthentication(string filePath) + { + if (filePath == null) + throw new ArgumentNullException(nameof(filePath)); + // Because this dump the whole file, let's make sure it is indeed the macaroon + if (!filePath.EndsWith(".macaroon", StringComparison.OrdinalIgnoreCase)) + throw new ArgumentException(message: "filePath is not a macaroon file", paramName: nameof(filePath)); + FilePath = filePath; + } + public string FilePath { get; set; } + public override void AddAuthentication(HttpRequestMessage httpRequest) + { + try + { + var bytes = File.ReadAllBytes(FilePath); + httpRequest.Headers.Add("Grpc-Metadata-macaroon", Encoders.Hex.EncodeData(bytes)); + } + catch + { + } + } + } + + public abstract void AddAuthentication(HttpRequestMessage httpRequest); + } +} diff --git a/BTCPayServer/Payments/Lightning/Lnd/LndRestSettings.cs b/BTCPayServer/Payments/Lightning/Lnd/LndRestSettings.cs new file mode 100644 index 000000000..a9250517e --- /dev/null +++ b/BTCPayServer/Payments/Lightning/Lnd/LndRestSettings.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Payments.Lightning.Lnd +{ + public class LndRestSettings + { + public LndRestSettings() + { + + } + public LndRestSettings(Uri uri) + { + Uri = uri; + } + public Uri Uri { get; set; } + /// + /// The SHA256 of the PEM certificate + /// + public byte[] CertificateThumbprint { get; set; } + public byte[] Macaroon { get; set; } + public bool AllowInsecure { get; set; } + public string MacaroonFilePath { get; set; } + + public LndAuthentication CreateLndAuthentication() + { + if (Macaroon != null) + return new LndAuthentication.FixedMacaroonAuthentication(Macaroon); + if (!string.IsNullOrEmpty(MacaroonFilePath)) + return new LndAuthentication.MacaroonFileAuthentication(MacaroonFilePath); + return LndAuthentication.NullAuthentication.Instance; + } + } +} diff --git a/BTCPayServer/Payments/Lightning/Lnd/LndSwaggerClient.partial.cs b/BTCPayServer/Payments/Lightning/Lnd/LndSwaggerClient.partial.cs index a12814ec6..02a4e6d1f 100644 --- a/BTCPayServer/Payments/Lightning/Lnd/LndSwaggerClient.partial.cs +++ b/BTCPayServer/Payments/Lightning/Lnd/LndSwaggerClient.partial.cs @@ -10,38 +10,28 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; +using NBitcoin.DataEncoders; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace BTCPayServer.Payments.Lightning.Lnd { - public class LndRestSettings - { - public LndRestSettings() - { - - } - public LndRestSettings(Uri uri) - { - Uri = uri; - } - public Uri Uri { get; set; } - /// - /// The SHA256 of the PEM certificate - /// - public byte[] CertificateThumbprint { get; set; } - public byte[] Macaroon { get; set; } - public bool AllowInsecure { get; set; } - } - public partial class LndSwaggerClient { public LndSwaggerClient(LndRestSettings settings) : this(settings.Uri.AbsoluteUri.TrimEnd('/'), CreateHttpClient(settings)) { _Settings = settings; + _Authentication = settings.CreateLndAuthentication(); } LndRestSettings _Settings; + LndAuthentication _Authentication; + + partial void PrepareRequest(HttpClient client, HttpRequestMessage request, string url) + { + _Authentication.AddAuthentication(request); + } + internal static HttpClient CreateHttpClient(LndRestSettings settings) { var handler = new HttpClientHandler @@ -60,7 +50,7 @@ namespace BTCPayServer.Payments.Lightning.Lnd }; } - if(settings.AllowInsecure) + if (settings.AllowInsecure) { handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; } @@ -69,14 +59,7 @@ namespace BTCPayServer.Payments.Lightning.Lnd if (settings.Uri.Scheme == "http") throw new InvalidOperationException("AllowInsecure is set to false, but the URI is not using https"); } - - var httpClient = new HttpClient(handler); - if (settings.Macaroon != null) - { - var macaroonHex = BitConverter.ToString(settings.Macaroon).Replace("-", "", StringComparison.InvariantCulture); - httpClient.DefaultRequestHeaders.Add("Grpc-Metadata-macaroon", macaroonHex); - } - return httpClient; + return new HttpClient(handler); } internal HttpClient CreateHttpClient()