diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index a3257db64..a45909c57 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -114,6 +114,15 @@ + + + $(IncludeRazorContentInPack) + + + $(IncludeRazorContentInPack) + + + PreserveNewest diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 6cf586451..43342468d 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -1,6 +1,8 @@ -using BTCPayServer.HostedServices; +using BTCPayServer.Configuration; +using BTCPayServer.HostedServices; using BTCPayServer.Models; using BTCPayServer.Models.ServerViewModels; +using BTCPayServer.Payments.Lightning; using BTCPayServer.Services; using BTCPayServer.Services.Mails; using BTCPayServer.Services.Rates; @@ -9,6 +11,7 @@ using BTCPayServer.Validations; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using NBitcoin.DataEncoders; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -27,16 +30,22 @@ namespace BTCPayServer.Controllers SettingsRepository _SettingsRepository; private BTCPayRateProviderFactory _RateProviderFactory; private StoreRepository _StoreRepository; + LightningConfigurationProvider _LnConfigProvider; + BTCPayServerOptions _Options; public ServerController(UserManager userManager, + Configuration.BTCPayServerOptions options, BTCPayRateProviderFactory rateProviderFactory, SettingsRepository settingsRepository, + LightningConfigurationProvider lnConfigProvider, Services.Stores.StoreRepository storeRepository) { + _Options = options; _UserManager = userManager; _SettingsRepository = settingsRepository; _RateProviderFactory = rateProviderFactory; _StoreRepository = storeRepository; + _LnConfigProvider = lnConfigProvider; } [Route("server/rates")] @@ -78,7 +87,7 @@ namespace BTCPayServer.Controllers try { var service = GetCoinaverageService(vm, true); - if(service != null) + if (service != null) await service.TestAuthAsync(); } catch @@ -225,6 +234,121 @@ namespace BTCPayServer.Controllers return View(settings); } + [Route("server/services")] + public IActionResult Services() + { + var result = new ServicesViewModel(); + foreach (var internalNode in _Options.InternalLightningByCryptoCode) + { + //Only BTC can be supported because gRPC does not allow http path rewriting. + if (internalNode.Key == "BTC" && GetExternalLNDConnectionString(internalNode.Value) != null) + { + result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel() + { + Crypto = internalNode.Key, + Type = "gRPC" + }); + } + } + return View(result); + } + + private LightningConnectionString GetExternalLNDConnectionString(LightningConnectionString value) + { + if (value.ConnectionType != LightningConnectionType.LndREST) + return null; + var external = new LightningConnectionString(); + external.ConnectionType = LightningConnectionType.LndREST; + external.BaseUri = _Options.ExternalUrl ?? value.BaseUri; + if (external.BaseUri.Scheme == "http" || value.AllowInsecure) + { + external.AllowInsecure = true; + } + + try + { + if (value.MacaroonFilePath != null) + external.Macaroon = System.IO.File.ReadAllBytes(value.MacaroonFilePath); + } + catch + { + return null; + } + if (value.Macaroon != null) + external.Macaroon = value.Macaroon; + + // If external url is provided, then we don't care about cert thumbprint + // because we override it at the reverse proxy level with a trusted certificate + if (_Options.ExternalUrl == null) + { + if (value.CertificateThumbprint != null) + { + external.CertificateThumbprint = value.CertificateThumbprint; + external.AllowInsecure = false; + } + } + return external; + } + + [Route("server/services/lnd-grpc/{cryptoCode}")] + public IActionResult LNDGRPCServices(string cryptoCode, ulong? secret) + { + if (!_Options.InternalLightningByCryptoCode.TryGetValue(cryptoCode.ToUpperInvariant(), out var connectionString)) + return NotFound(); + var model = new LNDGRPCServicesViewModel(); + var external = GetExternalLNDConnectionString(connectionString); + + model.Host = external.BaseUri.DnsSafeHost; + model.Port = external.BaseUri.Port; + model.SSL = external.BaseUri.Scheme == "https"; + if (external.CertificateThumbprint != null) + { + model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint); + } + if (external.Macaroon != null) + { + model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon); + } + if (secret != null) + { + var lnConfig = _LnConfigProvider.GetConfig(secret.Value); + if (lnConfig != null) + { + model.QRCode = $"config={this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{secret.Value}/lnd.config"; + } + } + return View(model); + } + [Route("lnd-config/{secret}/lnd.config")] + public IActionResult GetLNDConfig(ulong secret) + { + var conf = _LnConfigProvider.GetConfig(secret); + if (conf == null) + return NotFound(); + return Json(conf); + } + + [Route("server/services/lnd-grpc/{cryptoCode}")] + [HttpPost] + public IActionResult LNDGRPCServicesPOST(string cryptoCode) + { + if (!_Options.InternalLightningByCryptoCode.TryGetValue(cryptoCode.ToUpperInvariant(), out var connectionString)) + return NotFound(); + var external = GetExternalLNDConnectionString(connectionString); + LightningConfigurations confs = new LightningConfigurations(); + LightningConfiguration conf = new LightningConfiguration(); + conf.Type = "grpc"; + conf.CryptoCode = cryptoCode; + conf.Host = external.BaseUri.DnsSafeHost; + conf.Port = external.BaseUri.Port; + conf.SSL = external.BaseUri.Scheme == "https"; + conf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon); + conf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint); + confs.Configurations.Add(conf); + var secret = _LnConfigProvider.KeepConfig(confs); + return RedirectToAction(nameof(LNDGRPCServices), new { cryptoCode = cryptoCode, secret = secret }); + } + [Route("server/theme")] public async Task Theme() { @@ -248,7 +372,7 @@ namespace BTCPayServer.Controllers { try { - if(!model.Settings.IsComplete()) + if (!model.Settings.IsComplete()) { model.StatusMessage = "Error: Required fields missing"; return View(model); diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index ccfe52771..b645d60b0 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -92,6 +92,7 @@ namespace BTCPayServer.Hosting return opts.NetworkProvider; }); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/BTCPayServer/Models/ServerViewModels/LNDGRPCServicesViewModel.cs b/BTCPayServer/Models/ServerViewModels/LNDGRPCServicesViewModel.cs new file mode 100644 index 000000000..12c4074bb --- /dev/null +++ b/BTCPayServer/Models/ServerViewModels/LNDGRPCServicesViewModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Models.ServerViewModels +{ + public class LNDGRPCServicesViewModel + { + public string Host { get; set; } + public int Port { get; set; } + public bool SSL { get; set; } + public string Macaroon { get; set; } + public string CertificateThumbprint { get; set; } + public string QRCode { get; set; } + } +} diff --git a/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs b/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs new file mode 100644 index 000000000..a37ad2a24 --- /dev/null +++ b/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Models.ServerViewModels +{ + public class ServicesViewModel + { + public class LNDServiceViewModel + { + public string Crypto { get; set; } + public string Type { get; set; } + } + public List LNDServices { get; set; } = new List(); + } +} diff --git a/BTCPayServer/Payments/Lightning/LightningConnectionString.cs b/BTCPayServer/Payments/Lightning/LightningConnectionString.cs index 9f245b0c3..59e83af1b 100644 --- a/BTCPayServer/Payments/Lightning/LightningConnectionString.cs +++ b/BTCPayServer/Payments/Lightning/LightningConnectionString.cs @@ -353,7 +353,7 @@ namespace BTCPayServer.Payments.Lightning public LightningConnectionType ConnectionType { get; - private set; + set; } public byte[] Macaroon { get; set; } public string MacaroonFilePath { get; set; } diff --git a/BTCPayServer/Services/LightningConfigurationProvider.cs b/BTCPayServer/Services/LightningConfigurationProvider.cs new file mode 100644 index 000000000..b54f83efc --- /dev/null +++ b/BTCPayServer/Services/LightningConfigurationProvider.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NBitcoin; + +namespace BTCPayServer.Services +{ + public class LightningConfigurationProvider + { + ConcurrentDictionary _Map = new ConcurrentDictionary(); + public ulong KeepConfig(LightningConfigurations configuration) + { + CleanExpired(); + var secret = RandomUtils.GetUInt64(); + _Map.AddOrReplace(secret, (DateTimeOffset.UtcNow + TimeSpan.FromMinutes(10), configuration)); + return secret; + } + + public LightningConfigurations GetConfig(ulong secret) + { + CleanExpired(); + if (!_Map.TryGetValue(secret, out var value)) + return null; + return value.config; + } + + private void CleanExpired() + { + foreach(var item in _Map) + { + if(item.Value.expiration < DateTimeOffset.UtcNow) + { + _Map.TryRemove(item.Key, out var unused); + } + } + } + } + + public class LightningConfigurations + { + public List Configurations { get; set; } = new List(); + } + public class LightningConfiguration + { + public string Type { get; set; } + public string CryptoCode { get; set; } + public string Host { get; set; } + public int Port { get; set; } + public bool SSL { get; set; } + public string CertificateThumbprint { get; set; } + public string Macaroon { get; set; } + } +} diff --git a/BTCPayServer/Views/Server/LNDGRPCServices.cshtml b/BTCPayServer/Views/Server/LNDGRPCServices.cshtml new file mode 100644 index 000000000..991782582 --- /dev/null +++ b/BTCPayServer/Views/Server/LNDGRPCServices.cshtml @@ -0,0 +1,96 @@ +@model BTCPayServer.Models.ServerViewModels.LNDGRPCServicesViewModel +@{ + ViewData.SetActivePageAndTitle(ServerNavPages.Services); +} + + +

GRPC settings

+ +
+
+
+
+
+ +
+ +
+
+

+ BTCPay exposes gRPC services for outside consumption, you will find connection informaiton here.
+

+
+ +
+
QR Code connection
+

+ You can use this QR Code to connect your Zap wallet to your LND instance.
+ This QR Code is only valid for 10 minutes +

+
+
+ @if(Model.QRCode == null) + { +
+ +
+ } + else + { +
+
+ + } +
+ +
+
More details...
+

Alternatively, you can see the settings by clicking here

+
+
+
+ + +
+
+ + +
+
+ + +
+ @if(Model.Macaroon != null) + { +
+ + +
+ } + @if(Model.CertificateThumbprint != null) + { +
+ + +
+ } +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") + + @if(Model.QRCode != null) + { + + + } +} diff --git a/BTCPayServer/Views/Server/ServerNavPages.cs b/BTCPayServer/Views/Server/ServerNavPages.cs index 2f266c383..fb08dc50f 100644 --- a/BTCPayServer/Views/Server/ServerNavPages.cs +++ b/BTCPayServer/Views/Server/ServerNavPages.cs @@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Server { public enum ServerNavPages { - Index, Users, Rates, Emails, Policies, Theme, Hangfire + Index, Users, Rates, Emails, Policies, Theme, Hangfire, Services } } diff --git a/BTCPayServer/Views/Server/Services.cshtml b/BTCPayServer/Views/Server/Services.cshtml new file mode 100644 index 000000000..8e2673107 --- /dev/null +++ b/BTCPayServer/Views/Server/Services.cshtml @@ -0,0 +1,53 @@ +@model BTCPayServer.Models.ServerViewModels.ServicesViewModel +@{ + ViewData.SetActivePageAndTitle(ServerNavPages.Services); +} + + +

@ViewData["Title"]

+ +
+
+
+
+
+ +
+ @if(Model.LNDServices.Count != 0) + { +
+
+
LND nodes
+ You can get access to internal LND services here. For gRPC, only BTC is supported. +
+ +
+ + + + + + + + + + @foreach(var lnd in Model.LNDServices) + { + + + + + + } + +
CryptoAccess TypeActions
@lnd.Crypto@lnd.Type + See information +
+
+
+ } +
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/Server/_Nav.cshtml b/BTCPayServer/Views/Server/_Nav.cshtml index 6d4de07fb..5ec54ccf7 100644 --- a/BTCPayServer/Views/Server/_Nav.cshtml +++ b/BTCPayServer/Views/Server/_Nav.cshtml @@ -3,6 +3,7 @@ Rates Email server Policies + Services Theme Hangfire