Merge branch 'master' into mobile-working-branch

This commit is contained in:
Dennis Reimann 2024-12-10 11:04:11 +01:00
commit 9d207a968a
No known key found for this signature in database
GPG Key ID: 5009E1797F03F8D0
11 changed files with 192 additions and 33 deletions

View File

@ -2903,6 +2903,16 @@ namespace BTCPayServer.Tests
// Unauthenticated user can't access recent transactions
s.GoToUrl(keypadUrl);
s.Driver.ElementDoesNotExist(By.Id("RecentTransactionsToggle"));
// But they can generate invoices
s.Driver.FindElement(By.CssSelector(".keypad [data-key='1']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='2']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='3']")).Click();
s.Driver.FindElement(By.Id("pay-button")).Click();
s.Driver.WaitUntilAvailable(By.Id("Checkout"));
s.Driver.FindElement(By.Id("DetailsToggle")).Click();
s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat"));
Assert.Contains("1,23 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
}
[Fact]

View File

@ -937,10 +937,15 @@ namespace BTCPayServer.Controllers
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
model.TimeLeft = expiration.PrettyPrint();
if (_paymentModelExtensions.TryGetValue(paymentMethodId, out var extension) &&
_handlers.TryGetValue(paymentMethodId, out var h))
if (_handlers.TryGetValue(paymentMethodId, out var h))
{
extension.ModifyCheckoutModel(new CheckoutModelContext(model, store, storeBlob, invoice, Url, prompt, h));
var ctx = new CheckoutModelContext(model, store, storeBlob, invoice, Url, prompt, h);
if (_paymentModelExtensions.TryGetValue(paymentMethodId, out var extension))
{
extension.ModifyCheckoutModel(ctx);
}
foreach (var global in GlobalCheckoutModelExtensions)
global.ModifyCheckoutModel(ctx);
}
return model;
}

View File

@ -69,6 +69,7 @@ namespace BTCPayServer.Controllers
private readonly UriResolver _uriResolver;
public WebhookSender WebhookNotificationManager { get; }
public IEnumerable<IGlobalCheckoutModelExtension> GlobalCheckoutModelExtensions { get; }
public IStringLocalizer StringLocalizer { get; }
public UIInvoiceController(
@ -99,6 +100,7 @@ namespace BTCPayServer.Controllers
IAuthorizationService authorizationService,
TransactionLinkProviders transactionLinkProviders,
Dictionary<PaymentMethodId, ICheckoutModelExtension> paymentModelExtensions,
IEnumerable<IGlobalCheckoutModelExtension> globalCheckoutModelExtensions,
IStringLocalizer stringLocalizer,
PrettyNameProvider prettyName)
{
@ -124,6 +126,7 @@ namespace BTCPayServer.Controllers
_authorizationService = authorizationService;
_transactionLinkProviders = transactionLinkProviders;
_paymentModelExtensions = paymentModelExtensions;
GlobalCheckoutModelExtensions = globalCheckoutModelExtensions;
_prettyName = prettyName;
_fileService = fileService;
_uriResolver = uriResolver;

View File

@ -217,17 +217,25 @@ namespace BTCPayServer
return endpoint != null;
}
public static Uri GetServerUri(this ILightningClient client)
[Obsolete("Use GetServerUri(this ILightningClient client, string connectionString) instead")]
public static Uri GetServerUri(this ILightningClient client) => GetServerUri(client, client.ToString());
public static Uri GetServerUri(this ILightningClient client, string connectionString)
{
var kv = LightningConnectionStringHelper.ExtractValues(client.ToString(), out _);
if (client is IExtendedLightningClient { ServerUri: { } uri })
return uri;
var kv = client.ExtractValues(connectionString);
return !kv.TryGetValue("server", out var server) ? null : new Uri(server, UriKind.Absolute);
}
public static string GetDisplayName(this ILightningClient client)
[Obsolete("Use GetDisplayName(this ILightningClient client, string connectionString) instead")]
public static string GetDisplayName(this ILightningClient client) => GetDisplayName(client, client.ToString());
public static string GetDisplayName(this ILightningClient client, string connectionString)
{
LightningConnectionStringHelper.ExtractValues(client.ToString(), out var type);
if (client is IExtendedLightningClient { DisplayName: { } displayName })
return displayName;
var kv = client.ExtractValues(connectionString);
if (!kv.TryGetValue("type", out var type))
return "???";
var lncType = typeof(LightningConnectionType);
var fields = lncType.GetFields(BindingFlags.Public | BindingFlags.Static);
var field = fields.FirstOrDefault(f => f.GetValue(lncType)?.ToString() == type);
@ -236,9 +244,96 @@ namespace BTCPayServer
return attr?.Name ?? type;
}
public static bool IsSafe(this ILightningClient client)
private static bool TryParseLegacy(string str, out Dictionary<string, string> connectionString)
{
var kv = LightningConnectionStringHelper.ExtractValues(client.ToString(), out _);
if (str.StartsWith("/"))
{
str = "unix:" + str;
}
Dictionary<string, string> dictionary = new Dictionary<string, string>();
connectionString = null;
if (!Uri.TryCreate(str, UriKind.Absolute, out Uri result))
{
return false;
}
if (!new string[4] { "unix", "tcp", "http", "https" }.Contains(result.Scheme))
{
return false;
}
if (result.Scheme == "unix")
{
str = result.AbsoluteUri.Substring("unix:".Length);
while (str.Length >= 1 && str[0] == '/')
{
str = str.Substring(1);
}
result = new Uri("unix://" + str, UriKind.Absolute);
dictionary.Add("type", "clightning");
}
if (result.Scheme == "tcp")
{
dictionary.Add("type", "clightning");
}
if (result.Scheme == "http" || result.Scheme == "https")
{
string[] array = result.UserInfo.Split(':');
if (string.IsNullOrEmpty(result.UserInfo) || array.Length != 2)
{
return false;
}
dictionary.Add("type", "charge");
dictionary.Add("username", array[0]);
dictionary.Add("password", array[1]);
if (result.Scheme == "http")
{
dictionary.Add("allowinsecure", "true");
}
}
else if (!string.IsNullOrEmpty(result.UserInfo))
{
return false;
}
dictionary.Add("server", new UriBuilder(result)
{
UserName = "",
Password = ""
}.Uri.ToString());
connectionString = dictionary;
return true;
}
static Dictionary<string, string> ExtractValues(this ILightningClient client, string connectionString)
{
ArgumentNullException.ThrowIfNull(connectionString);
if (TryParseLegacy(connectionString, out var legacy))
return legacy;
string[] source = connectionString.Split(new char[1] { ';' }, StringSplitOptions.RemoveEmptyEntries);
var kv = new Dictionary<string, string>();
foreach (string item in source.Select((string p) => p.Trim()))
{
int num = item.IndexOf('=');
if (num == -1)
continue;
string text = item.Substring(0, num).Trim().ToLowerInvariant();
string value = item.Substring(num + 1).Trim();
kv.TryAdd(text, value);
}
return kv;
}
[Obsolete("Use IsSafe(this ILightningClient client, string connectionString) instead")]
public static bool IsSafe(this ILightningClient client) => IsSafe(client, client.ToString());
public static bool IsSafe(this ILightningClient client, string connectionString)
{
var kv = client.ExtractValues(connectionString);
if (kv.TryGetValue("cookiefilepath", out _) ||
kv.TryGetValue("macaroondirectorypath", out _) ||
kv.TryGetValue("macaroonfilepath", out _) )
@ -662,6 +757,9 @@ namespace BTCPayServer
return controller.View("PostRedirect", redirectVm);
}
public static string RemoveUserInfo(this Uri uri)
=> string.IsNullOrEmpty(uri.UserInfo) ? uri.ToString() : uri.ToString().Replace(uri.UserInfo, "***");
public static DataDirectories Configure(this DataDirectories dataDirectories, IConfiguration configuration)
{
var networkType = DefaultConfiguration.GetNetworkType(configuration);

View File

@ -0,0 +1,10 @@
namespace BTCPayServer.Payments
{
/// <summary>
/// <see cref="ModifyCheckoutModel"/> will always run when showing the checkout page
/// </summary>
public interface IGlobalCheckoutModelExtension
{
void ModifyCheckoutModel(CheckoutModelContext context);
}
}

View File

@ -0,0 +1,25 @@
#nullable enable
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
namespace BTCPayServer.Payments.Lightning
{
public interface IExtendedLightningClient : ILightningClient
{
/// <summary>
/// Used to validate the client configuration
/// </summary>
/// <returns></returns>
public Task<ValidationResult> Validate();
/// <summary>
/// The display name of this client (ie. LND (REST), Eclair, LNDhub)
/// </summary>
public string? DisplayName { get; }
/// <summary>
/// The server URI of this client (ie. http://localhost:8080)
/// </summary>
public Uri? ServerUri { get; }
}
}

View File

@ -1,5 +1,6 @@
#nullable enable
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -263,7 +264,16 @@ namespace BTCPayServer.Payments.Lightning
try
{
var client = _lightningClientFactory.Create(config.ConnectionString, _Network);
if (!client.IsSafe())
if (client is IExtendedLightningClient vlc)
{
var result = await vlc.Validate();
if (result != ValidationResult.Success)
{
validationContext.ModelState.AddModelError(nameof(config.ConnectionString), result?.ErrorMessage ?? "Invalid connection string");
return;
}
}
if (!client.IsSafe(config.ConnectionString))
{
var canManage = (await validationContext.AuthorizationService.AuthorizeAsync(validationContext.User, null,
new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded;
@ -274,6 +284,11 @@ namespace BTCPayServer.Payments.Lightning
}
}
}
catch (FormatException ex)
{
validationContext.ModelState.AddModelError(nameof(config.ConnectionString), ex.Message);
return;
}
catch
{
validationContext.ModelState.AddModelError(nameof(config.ConnectionString), "Invalid connection string");

View File

@ -505,27 +505,20 @@ retry:
public CancellationTokenSource? StopListeningCancellationTokenSource;
async Task Listen(CancellationToken cancellation)
{
Uri? uri = null;
string? logUrl = null;
string? uri = null;
try
{
var lightningClient = _lightningClientFactory.Create(ConnectionString, _network);
if(lightningClient is null)
return;
uri = lightningClient.GetServerUri();
logUrl = uri switch
{
null when LightningConnectionStringHelper.ExtractValues(ConnectionString, out var type) is not null => type,
null => string.Empty,
_ => string.IsNullOrEmpty(uri.UserInfo) ? uri.ToString() : uri.ToString().Replace(uri.UserInfo, "***")
};
Logs.PayServer.LogInformation("{CryptoCode} (Lightning): Start listening {Uri}", _network.CryptoCode, logUrl);
uri = lightningClient.GetServerUri(ConnectionString)?.RemoveUserInfo() ?? "";
Logs.PayServer.LogInformation("{CryptoCode} (Lightning): Start listening {Uri}", _network.CryptoCode, uri);
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("{CryptoCode} (Lightning): Could reconnect successfully to {Uri}", _network.CryptoCode, logUrl);
Logs.PayServer.LogInformation("{CryptoCode} (Lightning): Could reconnect successfully to {Uri}", _network.CryptoCode, uri);
}
_ErrorAlreadyLogged = false;
while (!_ListenedInvoices.IsEmpty)
@ -557,12 +550,12 @@ retry:
catch (Exception ex) when (!cancellation.IsCancellationRequested && !_ErrorAlreadyLogged)
{
_ErrorAlreadyLogged = true;
Logs.PayServer.LogError(ex, "{CryptoCode} (Lightning): Error while contacting {Uri}", _network.CryptoCode, logUrl);
Logs.PayServer.LogInformation("{CryptoCode} (Lightning): Stop listening {Uri}", _network.CryptoCode, logUrl);
Logs.PayServer.LogError(ex, "{CryptoCode} (Lightning): Error while contacting {Uri}", _network.CryptoCode, uri);
Logs.PayServer.LogInformation("{CryptoCode} (Lightning): Stop listening {Uri}", _network.CryptoCode, uri);
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { }
if (_ListenedInvoices.IsEmpty)
Logs.PayServer.LogInformation("{CryptoCode} (Lightning): No more invoice to listen on {Uri}, releasing the connection", _network.CryptoCode, logUrl);
Logs.PayServer.LogInformation("{CryptoCode} (Lightning): No more invoice to listen on {Uri}, releasing the connection", _network.CryptoCode, uri);
}
private uint256? GetPaymentHash(ListenedInvoice listenedInvoice)

View File

@ -434,9 +434,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
}
private async Task<bool> Throttle(string appId) =>
HttpContext.Connection is { RemoteIpAddress: { } addr } &&
await _rateLimitService.Throttle(ZoneLimits.PublicInvoices, addr.ToString(), HttpContext.RequestAborted) &&
!(await _authorizationService.AuthorizeAsync(HttpContext.User, appId, Policies.CanViewInvoices)).Succeeded;
!(await _authorizationService.AuthorizeAsync(HttpContext.User, appId, Policies.CanViewInvoices)).Succeeded &&
HttpContext.Connection is { RemoteIpAddress: { } addr } &&
!await _rateLimitService.Throttle(ZoneLimits.PublicInvoices, addr.ToString(), HttpContext.RequestAborted);
private JObject TryParseJObject(string posData)
{

View File

@ -19,8 +19,8 @@
@try
{
var client = LightningClientFactoryService.Create(Model.ConnectionString, NetworkProvider.GetNetwork<BTCPayNetwork>(Model.CryptoCode));
<span>@client.GetDisplayName()</span>
var uri = client.GetServerUri();
<span>@client.GetDisplayName(Model.ConnectionString)</span>
var uri = client.GetServerUri(Model.ConnectionString);
if (uri is not null)
{
<span>(@uri.Host)</span>

View File

@ -23,8 +23,8 @@
@try
{
var client = LightningClientFactoryService.Create(Model.ConnectionString, NetworkProvider.GetNetwork<BTCPayNetwork>(Model.CryptoCode));
<span>@client.GetDisplayName()</span>
var uri = client.GetServerUri();
<span>@client.GetDisplayName(Model.ConnectionString)</span>
var uri = client.GetServerUri(Model.ConnectionString);
if (uri is not null)
{
<span>(@uri.Host)</span>