Compare commits
26 Commits
v1.12.5-r0
...
v1.2.4
Author | SHA1 | Date | |
---|---|---|---|
02195ed096 | |||
562b96cf1f | |||
f44b54bc59 | |||
1889dec567 | |||
aa953ad6a6 | |||
6a7d434c89 | |||
cc812f96c9 | |||
6a4cb0e95c | |||
4c7149de95 | |||
532e847cdd | |||
3e3d4aea03 | |||
55bfecd4ed | |||
2e4d1f6d37 | |||
a3338b6f80 | |||
ade11faf94 | |||
401fbb6d9c | |||
e34f001bf8 | |||
7384cbd42d | |||
13e6675a23 | |||
cbabff755f | |||
cda46250fb | |||
929b51f814 | |||
c460d9e5c6 | |||
13a4130fde | |||
747e14ca74 | |||
50b297b048 |
@ -134,7 +134,7 @@ namespace BTCPayServer.Tests
|
||||
config.AppendLine($"torrcfile={TestUtils.GetTestDataFullPath("Tor/torrc")}");
|
||||
config.AppendLine($"socksendpoint={SocksEndpoint}");
|
||||
config.AppendLine($"debuglog=debug.log");
|
||||
|
||||
config.AppendLine($"nocsp={NoCSP.ToString().ToLowerInvariant()}");
|
||||
|
||||
if (!string.IsNullOrEmpty(SSHPassword) && string.IsNullOrEmpty(SSHKeyFile))
|
||||
config.AppendLine($"sshpassword={SSHPassword}");
|
||||
@ -283,6 +283,8 @@ namespace BTCPayServer.Tests
|
||||
public string SSHPassword { get; internal set; }
|
||||
public string SSHKeyFile { get; internal set; }
|
||||
public string SSHConnection { get; set; }
|
||||
public bool NoCSP { get; set; }
|
||||
|
||||
public T GetController<T>(string userId = null, string storeId = null, bool isAdmin = false) where T : Controller
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
|
@ -38,6 +38,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
Server.PayTester.NoCSP = true;
|
||||
await Server.StartAsync();
|
||||
|
||||
var windowSize = (Width: 1200, Height: 1000);
|
||||
|
@ -2904,6 +2904,21 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
Assert.Equal(50.51m, invoice5g.Amount);
|
||||
Assert.Equal(50.51m, (decimal)invoice5g.Metadata["taxIncluded"]);
|
||||
|
||||
var zeroInvoice = await greenfield.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
|
||||
{
|
||||
Amount = 0m,
|
||||
Currency = "USD"
|
||||
});
|
||||
Assert.Equal(InvoiceStatus.New, zeroInvoice.Status);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
zeroInvoice = await greenfield.GetInvoice(user.StoreId, zeroInvoice.Id);
|
||||
Assert.Equal(InvoiceStatus.Settled, zeroInvoice.Status);
|
||||
});
|
||||
|
||||
var zeroInvoicePM = await greenfield.GetInvoicePaymentMethods(user.StoreId, zeroInvoice.Id);
|
||||
Assert.Empty(zeroInvoicePM);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.1" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.12" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" />
|
||||
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
|
||||
|
@ -1,4 +1,3 @@
|
||||
@inject LinkGenerator linkGenerator
|
||||
@inject UserManager<ApplicationUser> UserManager
|
||||
@inject ISettingsRepository SettingsRepository
|
||||
@using BTCPayServer.HostedServices
|
||||
@ -56,47 +55,3 @@ else
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@{
|
||||
var disabled = (await SettingsRepository.GetPolicies()).DisableInstantNotifications;
|
||||
if (!disabled)
|
||||
{
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
disabled = user?.DisabledNotifications == "all";
|
||||
}
|
||||
}
|
||||
@if (!disabled)
|
||||
{
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
|
||||
|
||||
if (supportsWebSockets) {
|
||||
|
||||
var loc = window.location, ws_uri;
|
||||
if (loc.protocol === "https:") {
|
||||
ws_uri = "wss:";
|
||||
} else {
|
||||
ws_uri = "ws:";
|
||||
}
|
||||
ws_uri += "//" + loc.host;
|
||||
ws_uri += "@linkGenerator.GetPathByAction("SubscribeUpdates", "Notifications")";
|
||||
var newDataEndpoint = "@linkGenerator.GetPathByAction("GetNotificationDropdownUI", "Notifications")";
|
||||
|
||||
try {
|
||||
socket = new WebSocket(ws_uri);
|
||||
socket.onmessage = function (e) {
|
||||
$.get(newDataEndpoint, function(data){
|
||||
$("#notifications-nav-item").replaceWith($(data));
|
||||
});
|
||||
};
|
||||
socket.onerror = function (e) {
|
||||
console.error("Error while connecting to websocket for notifications (callback)", e);
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error("Error while connecting to websocket for notifications", e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Connection string to a PostgreSQL database", CommandOptionType.SingleValue);
|
||||
app.Option("--mysql", $"Connection string to a MySQL database", CommandOptionType.SingleValue);
|
||||
app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
|
||||
app.Option("--sqlitefile", $"File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
|
||||
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
|
||||
|
@ -448,20 +448,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (view == "modal")
|
||||
model.IsModal = true;
|
||||
|
||||
_CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue
|
||||
if (!string.IsNullOrEmpty(model.CustomCSSLink) &&
|
||||
Uri.TryCreate(model.CustomCSSLink, UriKind.Absolute, out var uri))
|
||||
{
|
||||
_CSP.Clear();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(model.CustomLogoLink) &&
|
||||
Uri.TryCreate(model.CustomLogoLink, UriKind.Absolute, out uri))
|
||||
{
|
||||
_CSP.Clear();
|
||||
}
|
||||
|
||||
return View(nameof(Checkout), model);
|
||||
}
|
||||
|
||||
@ -519,9 +505,11 @@ namespace BTCPayServer.Controllers
|
||||
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
|
||||
if (!paymentMethodDetails.Activated)
|
||||
{
|
||||
await _InvoiceRepository.ActivateInvoicePaymentMethod(_EventAggregator, _NetworkProvider,
|
||||
_paymentMethodHandlerDictionary, store, invoice, paymentMethod.GetId());
|
||||
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
|
||||
if (await _InvoiceRepository.ActivateInvoicePaymentMethod(_EventAggregator, _NetworkProvider,
|
||||
_paymentMethodHandlerDictionary, store, invoice, paymentMethod.GetId()))
|
||||
{
|
||||
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
|
||||
}
|
||||
}
|
||||
var dto = invoice.EntityToDTO();
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
|
@ -32,7 +32,6 @@ namespace BTCPayServer.Controllers
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
readonly InvoiceRepository _InvoiceRepository;
|
||||
readonly ContentSecurityPolicies _CSP;
|
||||
readonly RateFetcher _RateProvider;
|
||||
readonly StoreRepository _StoreRepository;
|
||||
readonly UserManager<ApplicationUser> _UserManager;
|
||||
@ -72,7 +71,6 @@ namespace BTCPayServer.Controllers
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_paymentHostedService = paymentHostedService;
|
||||
WebhookNotificationManager = webhookNotificationManager;
|
||||
_CSP = csp;
|
||||
_languageService = languageService;
|
||||
}
|
||||
|
||||
@ -257,39 +255,48 @@ namespace BTCPayServer.Controllers
|
||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
|
||||
// This loop ends with .ToList so we are querying all payment methods at once
|
||||
// instead of sequentially to improve response time
|
||||
foreach (var o in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId) && _paymentMethodHandlerDictionary.Support(s.PaymentId))
|
||||
.Select(c =>
|
||||
(Handler: _paymentMethodHandlerDictionary[c.PaymentId],
|
||||
SupportedPaymentMethod: c,
|
||||
Network: _NetworkProvider.GetNetwork<BTCPayNetworkBase>(c.PaymentId.CryptoCode)))
|
||||
.Where(c => c.Network != null)
|
||||
.Select(o =>
|
||||
(SupportedPaymentMethod: o.SupportedPaymentMethod,
|
||||
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store, logs)))
|
||||
.ToList())
|
||||
{
|
||||
var paymentMethod = await o.PaymentMethod;
|
||||
if (paymentMethod == null)
|
||||
continue;
|
||||
supported.Add(o.SupportedPaymentMethod);
|
||||
paymentMethods.Add(paymentMethod);
|
||||
}
|
||||
bool noNeedForMethods = entity.Type != InvoiceType.TopUp && entity.Price == 0m;
|
||||
|
||||
if (supported.Count == 0)
|
||||
if (!noNeedForMethods)
|
||||
{
|
||||
StringBuilder errors = new StringBuilder();
|
||||
if (!store.GetSupportedPaymentMethods(_NetworkProvider).Any())
|
||||
errors.AppendLine("Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/WalletSetup/)");
|
||||
foreach (var error in logs.ToList())
|
||||
|
||||
// This loop ends with .ToList so we are querying all payment methods at once
|
||||
// instead of sequentially to improve response time
|
||||
foreach (var o in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId) &&
|
||||
_paymentMethodHandlerDictionary.Support(s.PaymentId))
|
||||
.Select(c =>
|
||||
(Handler: _paymentMethodHandlerDictionary[c.PaymentId],
|
||||
SupportedPaymentMethod: c,
|
||||
Network: _NetworkProvider.GetNetwork<BTCPayNetworkBase>(c.PaymentId.CryptoCode)))
|
||||
.Where(c => c.Network != null)
|
||||
.Select(o =>
|
||||
(SupportedPaymentMethod: o.SupportedPaymentMethod,
|
||||
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler,
|
||||
o.SupportedPaymentMethod, o.Network, entity, store, logs)))
|
||||
.ToList())
|
||||
{
|
||||
errors.AppendLine(error.ToString());
|
||||
var paymentMethod = await o.PaymentMethod;
|
||||
if (paymentMethod == null)
|
||||
continue;
|
||||
supported.Add(o.SupportedPaymentMethod);
|
||||
paymentMethods.Add(paymentMethod);
|
||||
}
|
||||
throw new BitpayHttpException(400, errors.ToString());
|
||||
}
|
||||
|
||||
if (supported.Count == 0)
|
||||
{
|
||||
StringBuilder errors = new StringBuilder();
|
||||
if (!store.GetSupportedPaymentMethods(_NetworkProvider).Any())
|
||||
errors.AppendLine(
|
||||
"Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/WalletSetup/)");
|
||||
foreach (var error in logs.ToList())
|
||||
{
|
||||
errors.AppendLine(error.ToString());
|
||||
}
|
||||
|
||||
throw new BitpayHttpException(400, errors.ToString());
|
||||
}
|
||||
}
|
||||
entity.SetSupportedPaymentMethods(supported);
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
foreach (var app in await getAppsTaggingStore)
|
||||
|
@ -195,7 +195,25 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private void AdjustVMForAuthorization(AuthorizeApiKeysViewModel vm)
|
||||
{
|
||||
var parsedPermissions = Permission.ToPermissions(vm.Permissions?.Split(';')??Array.Empty<string>()).GroupBy(permission => permission.Policy);
|
||||
var permissions = vm.Permissions?.Split(';') ?? Array.Empty<string>();
|
||||
var permissionsWithStoreIDs = new List<string>();
|
||||
/**
|
||||
* Go over each permission and associated store IDs and
|
||||
* join them so that permission for a specific store is parsed correctly
|
||||
*/
|
||||
for (var i = 0; i < permissions.Length; i++) {
|
||||
var currPerm = permissions[i];
|
||||
var storeIds = vm.PermissionValues[i].SpecificStores.ToArray();
|
||||
if (storeIds.Length > 0) {
|
||||
for (var x = 0; x < storeIds.Length; x++) {
|
||||
permissionsWithStoreIDs.Add($"{currPerm}:{storeIds[x]}");
|
||||
}
|
||||
} else {
|
||||
permissionsWithStoreIDs.Add(currPerm);
|
||||
}
|
||||
}
|
||||
|
||||
var parsedPermissions = Permission.ToPermissions(permissionsWithStoreIDs.ToArray()).GroupBy(permission => permission.Policy);
|
||||
|
||||
for (var index = vm.PermissionValues.Count - 1; index >= 0; index--)
|
||||
{
|
||||
@ -210,6 +228,14 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else if (wanted?.Any() ?? false)
|
||||
{
|
||||
var commandParts = vm.Command?.Split(':', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
|
||||
var command = commandParts.Length > 1 ? commandParts[1] : null;
|
||||
var isPerformingAnAction = command == "change-store-mode" || command == "add-store";
|
||||
// Don't want to accidentally change mode for the user if they are explicitly performing some action
|
||||
if (isPerformingAnAction) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (vm.SelectiveStores && Policies.IsStorePolicy(permissionValue.Permission) &&
|
||||
wanted.Any(permission => !string.IsNullOrEmpty(permission.Scope)))
|
||||
{
|
||||
@ -358,6 +384,12 @@ namespace BTCPayServer.Controllers
|
||||
permissionValueItem.StoreMode = permissionValueItem.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific
|
||||
? AddApiKeyViewModel.ApiKeyStoreMode.AllStores
|
||||
: AddApiKeyViewModel.ApiKeyStoreMode.Specific;
|
||||
// Reset values for "all stores" option to their original values
|
||||
if (permissionValueItem.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
|
||||
{
|
||||
permissionValueItem.SpecificStores = new List<string>();
|
||||
permissionValueItem.Value = true;
|
||||
}
|
||||
|
||||
if (permissionValueItem.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific &&
|
||||
!permissionValueItem.SpecificStores.Any() && viewModel.Stores.Any())
|
||||
|
@ -364,7 +364,7 @@ askdevice:
|
||||
|
||||
private static bool IsTrezorT(HwiEnumerateEntry deviceEntry)
|
||||
{
|
||||
return (deviceEntry.Model == HardwareWalletModels.Trezor_T || deviceEntry.Model == HardwareWalletModels.Trezor_T_Simulator);
|
||||
return deviceEntry.Model.Contains("Trezor_T", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public StoreData CurrentStore
|
||||
|
@ -52,7 +52,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
||||
|
||||
public bool CanHandle(PaymentMethodId paymentMethod)
|
||||
{
|
||||
return paymentMethod.PaymentType == BitcoinPaymentType.Instance &&
|
||||
return paymentMethod?.PaymentType == BitcoinPaymentType.Instance &&
|
||||
_btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethod.CryptoCode)?.ReadonlyWallet is false;
|
||||
}
|
||||
|
||||
@ -89,6 +89,10 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
||||
if (payout?.Proof is null)
|
||||
return null;
|
||||
var paymentMethodId = payout.GetPaymentMethodId();
|
||||
if (paymentMethodId is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var raw = JObject.Parse(Encoding.UTF8.GetString(payout.Proof));
|
||||
if (raw.TryGetValue("proofType", StringComparison.InvariantCultureIgnoreCase, out var proofType) &&
|
||||
proofType.Value<string>() == ManualPayoutProof.Type)
|
||||
@ -161,7 +165,9 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
||||
.Include(p => p.PullPaymentData.StoreData)
|
||||
.Where(p => payoutIds.Contains(p.Id))
|
||||
.Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingPayment)
|
||||
.ToListAsync()).Where(data => CanHandle(PaymentMethodId.Parse(data.PaymentMethodId)))
|
||||
.ToListAsync()).Where(data =>
|
||||
PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) &&
|
||||
CanHandle(paymentMethodId))
|
||||
.Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple=> tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == false);
|
||||
foreach (var valueTuple in payouts)
|
||||
{
|
||||
@ -185,7 +191,9 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
||||
.Include(p => p.PullPaymentData.StoreData)
|
||||
.Where(p => payoutIds.Contains(p.Id))
|
||||
.Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingPayment)
|
||||
.ToListAsync()).Where(data => CanHandle(PaymentMethodId.Parse(data.PaymentMethodId)))
|
||||
.ToListAsync()).Where(data =>
|
||||
PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) &&
|
||||
CanHandle(paymentMethodId))
|
||||
.Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple=> tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == true);
|
||||
foreach (var valueTuple in payouts)
|
||||
{
|
||||
|
@ -28,7 +28,7 @@ namespace BTCPayServer.Data
|
||||
|
||||
public static PaymentMethodId GetPaymentMethodId(this PayoutData data)
|
||||
{
|
||||
return PaymentMethodId.Parse(data.PaymentMethodId);
|
||||
return PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId)? paymentMethodId : null;
|
||||
}
|
||||
public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers)
|
||||
{
|
||||
|
@ -171,7 +171,8 @@ namespace BTCPayServer.Data
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (ExcludedPaymentMethods == null || ExcludedPaymentMethods.Length == 0)
|
||||
return PaymentFilter.Never();
|
||||
return PaymentFilter.Any(ExcludedPaymentMethods.Select(p => PaymentFilter.WhereIs(PaymentMethodId.Parse(p))).ToArray());
|
||||
|
||||
return PaymentFilter.Any(ExcludedPaymentMethods.ParsePaymentMethodIds().Select(PaymentFilter.WhereIs).ToArray());
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,8 @@ namespace BTCPayServer.Data
|
||||
public static PaymentMethodId[] GetEnabledPaymentIds(this StoreData storeData, BTCPayNetworkProvider networks)
|
||||
{
|
||||
var excludeFilter = storeData.GetStoreBlob().GetExcludedPaymentMethods();
|
||||
var paymentMethodIds = storeData.GetSupportedPaymentMethods(networks).Select(p => p.PaymentId)
|
||||
var paymentMethodIds = storeData.GetSupportedPaymentMethods(networks)
|
||||
.Select(p => p.PaymentId)
|
||||
.Where(a => !excludeFilter.Match(a))
|
||||
.OrderByDescending(a => a.CryptoCode == "BTC")
|
||||
.ThenBy(a => a.CryptoCode)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@ -482,6 +483,15 @@ namespace BTCPayServer
|
||||
"Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
return result;
|
||||
}
|
||||
|
||||
public static PaymentMethodId[] ParsePaymentMethodIds(this string[] paymentMethods)
|
||||
{
|
||||
return paymentMethods.Select(s =>
|
||||
{
|
||||
PaymentMethodId.TryParse(s, out var parsed);
|
||||
return parsed;
|
||||
}).Where(id => id != null).ToArray();
|
||||
}
|
||||
|
||||
public static DataDirectories Configure(this DataDirectories dataDirectories, IConfiguration configuration)
|
||||
{
|
||||
|
@ -7,13 +7,32 @@ using Microsoft.AspNetCore.Mvc.Filters;
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public interface IContentSecurityPolicy : IFilterMetadata { }
|
||||
public enum CSPTemplate
|
||||
{
|
||||
AntiXSS
|
||||
}
|
||||
public class ContentSecurityPolicyAttribute : Attribute, IActionFilter, IContentSecurityPolicy
|
||||
{
|
||||
public ContentSecurityPolicyAttribute()
|
||||
{
|
||||
|
||||
}
|
||||
public ContentSecurityPolicyAttribute(CSPTemplate template)
|
||||
{
|
||||
if (template == CSPTemplate.AntiXSS)
|
||||
{
|
||||
AutoSelf = false;
|
||||
FixWebsocket = false;
|
||||
UnsafeInline = false;
|
||||
ScriptSrc = "'self' 'unsafe-eval'"; // unsafe-eval needed for vue
|
||||
}
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
public bool AutoSelf { get; set; } = true;
|
||||
public bool UnsafeInline { get; set; } = true;
|
||||
public bool FixWebsocket { get; set; } = true;
|
||||
@ -22,83 +41,79 @@ namespace BTCPayServer.Filters
|
||||
public string DefaultSrc { get; set; }
|
||||
public string StyleSrc { get; set; }
|
||||
public string ScriptSrc { get; set; }
|
||||
public string ManifestSrc { get; set; }
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (context.IsEffectivePolicy<IContentSecurityPolicy>(this))
|
||||
if (!context.IsEffectivePolicy<IContentSecurityPolicy>(this) || !Enabled)
|
||||
return;
|
||||
var policies = context.HttpContext.RequestServices.GetService(typeof(ContentSecurityPolicies)) as ContentSecurityPolicies;
|
||||
if (policies == null)
|
||||
return;
|
||||
if (DefaultSrc != null)
|
||||
{
|
||||
var policies = context.HttpContext.RequestServices.GetService(typeof(ContentSecurityPolicies)) as ContentSecurityPolicies;
|
||||
if (policies == null)
|
||||
return;
|
||||
if (DefaultSrc != null)
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("default-src", DefaultSrc));
|
||||
}
|
||||
if (UnsafeInline)
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("script-src", "'unsafe-inline'"));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(FontSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("font-src", FontSrc));
|
||||
}
|
||||
policies.Add(new ConsentSecurityPolicy("default-src", DefaultSrc));
|
||||
}
|
||||
if (UnsafeInline)
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("script-src", "'unsafe-inline'"));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(FontSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("font-src", FontSrc));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(ManifestSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("manifest-src", FontSrc));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ImgSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("img-src", ImgSrc));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(ImgSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("img-src", ImgSrc));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(StyleSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("style-src", StyleSrc));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(StyleSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("style-src", StyleSrc));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ScriptSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("script-src", ScriptSrc));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(ScriptSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("script-src", ScriptSrc));
|
||||
}
|
||||
|
||||
if (FixWebsocket && AutoSelf) // Self does not match wss:// and ws:// :(
|
||||
{
|
||||
var request = context.HttpContext.Request;
|
||||
if (FixWebsocket && AutoSelf) // Self does not match wss:// and ws:// :(
|
||||
{
|
||||
var request = context.HttpContext.Request;
|
||||
|
||||
var url = string.Concat(
|
||||
request.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ? "ws" : "wss",
|
||||
"://",
|
||||
request.Host.ToUriComponent(),
|
||||
request.PathBase.ToUriComponent());
|
||||
policies.Add(new ConsentSecurityPolicy("connect-src", url));
|
||||
}
|
||||
var url = string.Concat(
|
||||
request.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ? "ws" : "wss",
|
||||
"://",
|
||||
request.Host.ToUriComponent(),
|
||||
request.PathBase.ToUriComponent());
|
||||
policies.Add(new ConsentSecurityPolicy("connect-src", url));
|
||||
}
|
||||
|
||||
context.HttpContext.Response.OnStarting(() =>
|
||||
context.HttpContext.Response.OnStarting(() =>
|
||||
{
|
||||
if (!policies.HasRules)
|
||||
return Task.CompletedTask;
|
||||
if (AutoSelf)
|
||||
{
|
||||
if (!policies.HasRules)
|
||||
return Task.CompletedTask;
|
||||
if (AutoSelf)
|
||||
bool hasSelf = false;
|
||||
foreach (var group in policies.Rules.GroupBy(p => p.Name))
|
||||
{
|
||||
bool hasSelf = false;
|
||||
foreach (var group in policies.Rules.GroupBy(p => p.Name))
|
||||
hasSelf = group.Any(g => g.Value.Contains("'self'", StringComparison.OrdinalIgnoreCase));
|
||||
if (!hasSelf && !group.Any(g => g.Value.Contains("'none'", StringComparison.OrdinalIgnoreCase) ||
|
||||
g.Value.Contains("*", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
hasSelf = group.Any(g => g.Value.Contains("'self'", StringComparison.OrdinalIgnoreCase));
|
||||
if (!hasSelf && !group.Any(g => g.Value.Contains("'none'", StringComparison.OrdinalIgnoreCase) ||
|
||||
g.Value.Contains("*", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy(group.Key, "'self'"));
|
||||
hasSelf = true;
|
||||
}
|
||||
if (hasSelf)
|
||||
{
|
||||
foreach (var authorized in policies.Authorized)
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy(group.Key, authorized));
|
||||
}
|
||||
}
|
||||
policies.Add(new ConsentSecurityPolicy(group.Key, "'self'"));
|
||||
}
|
||||
}
|
||||
context.HttpContext.Response.SetHeader("Content-Security-Policy", policies.ToString());
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
context.HttpContext.Response.SetHeader("Content-Security-Policy", policies.ToString());
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,22 +22,6 @@ namespace BTCPayServer.HostedServices
|
||||
if (policies != null)
|
||||
{
|
||||
var theme = settingsRepository.GetTheme().GetAwaiter().GetResult();
|
||||
if (theme.CreativeStartCssUri != null && Uri.TryCreate(theme.CreativeStartCssUri, UriKind.Absolute, out var uri))
|
||||
{
|
||||
policies.Clear();
|
||||
}
|
||||
if (theme.BootstrapCssUri != null && Uri.TryCreate(theme.BootstrapCssUri, UriKind.Absolute, out uri))
|
||||
{
|
||||
policies.Clear();
|
||||
}
|
||||
if (theme.ThemeCssUri != null && Uri.TryCreate(theme.ThemeCssUri, UriKind.Absolute, out uri))
|
||||
{
|
||||
policies.Clear();
|
||||
}
|
||||
if (theme.CustomThemeCssUri != null && Uri.TryCreate(theme.CustomThemeCssUri, UriKind.Absolute, out uri))
|
||||
{
|
||||
policies.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,8 +86,24 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
var allPaymentMethods = invoice.GetPaymentMethods();
|
||||
var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting);
|
||||
if (paymentMethod == null)
|
||||
if (allPaymentMethods.Any() && paymentMethod == null)
|
||||
return;
|
||||
if (accounting is null && invoice.Price is 0m)
|
||||
{
|
||||
accounting = new PaymentMethodAccounting()
|
||||
{
|
||||
Due = Money.Zero,
|
||||
Paid = Money.Zero,
|
||||
CryptoPaid = Money.Zero,
|
||||
DueUncapped = Money.Zero,
|
||||
NetworkFee = Money.Zero,
|
||||
TotalDue = Money.Zero,
|
||||
TxCount = 0,
|
||||
TxRequired = 0,
|
||||
MinimumTotalDue = Money.Zero,
|
||||
NetworkFeeAlreadyPaid = Money.Zero
|
||||
};
|
||||
}
|
||||
if (invoice.Status == InvoiceStatusLegacy.New || invoice.Status == InvoiceStatusLegacy.Expired)
|
||||
{
|
||||
var isPaid = invoice.IsUnsetTopUp() ?
|
||||
@ -153,7 +169,9 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
if (invoice.Status == InvoiceStatusLegacy.Paid)
|
||||
{
|
||||
var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy));
|
||||
var confirmedAccounting =
|
||||
paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy)) ??
|
||||
accounting;
|
||||
|
||||
if (// Is after the monitoring deadline
|
||||
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
||||
@ -177,7 +195,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
if (invoice.Status == InvoiceStatusLegacy.Confirmed)
|
||||
{
|
||||
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p));
|
||||
var completedAccounting = paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p)) ??
|
||||
accounting;
|
||||
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Completed));
|
||||
|
@ -286,8 +286,13 @@ namespace BTCPayServer.HostedServices
|
||||
req.Completion.SetResult(PayoutApproval.Result.OldRevision);
|
||||
return;
|
||||
}
|
||||
if (!PaymentMethodId.TryParse(payout.PaymentMethodId, out var paymentMethod))
|
||||
{
|
||||
req.Completion.SetResult(PayoutApproval.Result.NotFound);
|
||||
return;
|
||||
}
|
||||
payout.State = PayoutState.AwaitingPayment;
|
||||
var paymentMethod = PaymentMethodId.Parse(payout.PaymentMethodId);
|
||||
|
||||
if (paymentMethod.CryptoCode == payout.PullPaymentData.GetBlob().Currency)
|
||||
req.Rate = 1.0m;
|
||||
var cryptoAmount = payoutBlob.Amount / req.Rate;
|
||||
|
@ -114,14 +114,8 @@ namespace BTCPayServer.Hosting
|
||||
o.Filters.Add(new XXSSProtectionAttribute());
|
||||
o.Filters.Add(new ReferrerPolicyAttribute("same-origin"));
|
||||
o.ModelBinderProviders.Insert(0, new ModelBinders.DefaultModelBinderProvider());
|
||||
//o.Filters.Add(new ContentSecurityPolicyAttribute()
|
||||
//{
|
||||
// FontSrc = "'self' https://fonts.gstatic.com/",
|
||||
// ImgSrc = "'self' data:",
|
||||
// DefaultSrc = "'none'",
|
||||
// StyleSrc = "'self' 'unsafe-inline'",
|
||||
// ScriptSrc = "'self' 'unsafe-inline'"
|
||||
//});
|
||||
if (!Configuration.GetOrDefault<bool>("nocsp", false))
|
||||
o.Filters.Add(new ContentSecurityPolicyAttribute(CSPTemplate.AntiXSS));
|
||||
})
|
||||
.ConfigureApiBehaviorOptions(options =>
|
||||
{
|
||||
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NBitcoin.Crypto;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
@ -9,6 +10,8 @@ namespace BTCPayServer.Security
|
||||
{
|
||||
public ConsentSecurityPolicy(string name, string value)
|
||||
{
|
||||
if (value.Contains(';', StringComparison.OrdinalIgnoreCase))
|
||||
throw new FormatException();
|
||||
_Value = value;
|
||||
_Name = name;
|
||||
}
|
||||
@ -67,10 +70,41 @@ namespace BTCPayServer.Security
|
||||
}
|
||||
|
||||
readonly HashSet<ConsentSecurityPolicy> _Policies = new HashSet<ConsentSecurityPolicy>();
|
||||
|
||||
/// <summary>
|
||||
/// Allow a specific script as event handler
|
||||
/// </summary>
|
||||
/// <param name="script"></param>
|
||||
public void AllowUnsafeHashes(string script)
|
||||
{
|
||||
if (script is null)
|
||||
throw new ArgumentNullException(nameof(script));
|
||||
var sha = GetSha256(script);
|
||||
Add("script-src", $"'unsafe-hashes'");
|
||||
Add("script-src", $"'sha256-{sha}'");
|
||||
}
|
||||
/// <summary>
|
||||
/// Allow the injection of script tag with the following script
|
||||
/// </summary>
|
||||
/// <param name="script"></param>
|
||||
public void AllowInline(string script)
|
||||
{
|
||||
if (script is null)
|
||||
throw new ArgumentNullException(nameof(script));
|
||||
var sha = GetSha256(script);
|
||||
Add("script-src", $"'sha256-{sha}'");
|
||||
}
|
||||
static string GetSha256(string script)
|
||||
{
|
||||
return Convert.ToBase64String(Hashes.SHA256(Encoding.UTF8.GetBytes(script.Replace("\r\n", "\n", StringComparison.Ordinal))));
|
||||
}
|
||||
|
||||
public void Add(string name, string value)
|
||||
{
|
||||
Add(new ConsentSecurityPolicy(name, value));
|
||||
}
|
||||
public void Add(ConsentSecurityPolicy policy)
|
||||
{
|
||||
if (_Policies.Any(p => p.Name == policy.Name && p.Value == policy.Name))
|
||||
return;
|
||||
_Policies.Add(policy);
|
||||
}
|
||||
|
||||
@ -87,34 +121,19 @@ namespace BTCPayServer.Security
|
||||
{
|
||||
value.Append(';');
|
||||
}
|
||||
List<string> values = new List<string>();
|
||||
HashSet<string> values = new HashSet<string>();
|
||||
List<string> valuesList = new List<string>();
|
||||
values.Add(group.Key);
|
||||
valuesList.Add(group.Key);
|
||||
foreach (var v in group)
|
||||
{
|
||||
values.Add(v.Value);
|
||||
if (values.Add(v.Value))
|
||||
valuesList.Add(v.Value);
|
||||
}
|
||||
foreach (var i in authorized)
|
||||
{
|
||||
values.Add(i);
|
||||
}
|
||||
value.Append(String.Join(" ", values.OfType<object>().ToArray()));
|
||||
value.Append(String.Join(" ", valuesList.OfType<object>().ToArray()));
|
||||
firstGroup = false;
|
||||
}
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
internal void Clear()
|
||||
{
|
||||
authorized.Clear();
|
||||
_Policies.Clear();
|
||||
}
|
||||
|
||||
readonly HashSet<string> authorized = new HashSet<string>();
|
||||
internal void AddAllAuthorized(string v)
|
||||
{
|
||||
authorized.Add(v);
|
||||
}
|
||||
|
||||
public IEnumerable<string> Authorized => authorized;
|
||||
}
|
||||
}
|
||||
|
@ -289,7 +289,10 @@ namespace BTCPayServer.Services.Invoices
|
||||
JObject strategies = JObject.Parse(DerivationStrategies);
|
||||
foreach (var strat in strategies.Properties())
|
||||
{
|
||||
var paymentMethodId = PaymentMethodId.Parse(strat.Name);
|
||||
if (!PaymentMethodId.TryParse(strat.Name, out var paymentMethodId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var network = Networks.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode);
|
||||
if (network != null)
|
||||
{
|
||||
|
@ -10,11 +10,13 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
public static class InvoiceExtensions
|
||||
{
|
||||
|
||||
public static async Task ActivateInvoicePaymentMethod(this InvoiceRepository invoiceRepository,
|
||||
public static async Task<bool> ActivateInvoicePaymentMethod(this InvoiceRepository invoiceRepository,
|
||||
EventAggregator eventAggregator, BTCPayNetworkProvider btcPayNetworkProvider, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
StoreData store,InvoiceEntity invoice, PaymentMethodId paymentMethodId)
|
||||
{
|
||||
if (invoice.GetInvoiceState().Status != InvoiceStatusLegacy.New)
|
||||
return false;
|
||||
bool success = false;
|
||||
var eligibleMethodToActivate = invoice.GetPaymentMethod(paymentMethodId);
|
||||
if (!eligibleMethodToActivate.GetPaymentMethodDetails().Activated)
|
||||
{
|
||||
@ -34,6 +36,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
eligibleMethodToActivate.SetPaymentMethodDetails(newDetails);
|
||||
await invoiceRepository.UpdateInvoicePaymentMethod(invoice.Id, eligibleMethodToActivate);
|
||||
eventAggregator.Publish(new InvoicePaymentMethodActivated(paymentMethodId, invoice));
|
||||
eventAggregator.Publish(new InvoiceNeedUpdateEvent(invoice.Id));
|
||||
success = true;
|
||||
}
|
||||
catch (PaymentMethodUnavailableException ex)
|
||||
{
|
||||
@ -45,8 +49,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
|
||||
await invoiceRepository.AddInvoiceLogs(invoice.Id, logs);
|
||||
eventAggregator.Publish(new InvoiceNeedUpdateEvent(invoice.Id));
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
}
|
||||
|
144
BTCPayServer/TagHelpers.cs
Normal file
144
BTCPayServer/TagHelpers.cs
Normal file
@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
|
||||
namespace BTCPayServer.TagHelpers
|
||||
{
|
||||
[HtmlTargetElement("srv-model")]
|
||||
public class SrvModel : TagHelper
|
||||
{
|
||||
private readonly Safe _safe;
|
||||
private readonly ContentSecurityPolicies _csp;
|
||||
|
||||
public SrvModel(Safe safe, ContentSecurityPolicies csp)
|
||||
{
|
||||
_safe = safe;
|
||||
_csp = csp;
|
||||
}
|
||||
public string VarName { get; set; } = "srvModel";
|
||||
public object Model { get; set; }
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
output.TagName = "script";
|
||||
output.TagMode = TagMode.StartTagAndEndTag;
|
||||
output.Attributes.Add(new TagHelperAttribute("type", "text/javascript"));
|
||||
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
|
||||
output.Attributes.Add(new TagHelperAttribute("nonce", nonce));
|
||||
_csp.Add("script-src", $"'nonce-{nonce}'");
|
||||
output.Content.SetHtmlContent($"var {VarName} = {_safe.Json(Model)};");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a nonce-* so the inline-script can pass CSP rule when they are rendered server-side
|
||||
/// </summary>
|
||||
[HtmlTargetElement("script")]
|
||||
public class CSPInlineScriptTagHelper : TagHelper
|
||||
{
|
||||
private readonly ContentSecurityPolicies _csp;
|
||||
|
||||
public CSPInlineScriptTagHelper(ContentSecurityPolicies csp)
|
||||
{
|
||||
_csp = csp;
|
||||
}
|
||||
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
if (output.Attributes.ContainsName("src"))
|
||||
return;
|
||||
if (output.Attributes.TryGetAttribute("type", out var attr))
|
||||
{
|
||||
if (attr.Value?.ToString() != "text/javascript")
|
||||
return;
|
||||
}
|
||||
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
|
||||
output.Attributes.Add(new TagHelperAttribute("nonce", nonce));
|
||||
_csp.Add("script-src", $"'nonce-{nonce}'");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add 'unsafe-hashes' and sha256- to allow inline event handlers in CSP
|
||||
/// </summary>
|
||||
[HtmlTargetElement(Attributes = "onclick")]
|
||||
[HtmlTargetElement(Attributes = "onkeypress")]
|
||||
[HtmlTargetElement(Attributes = "onchange")]
|
||||
[HtmlTargetElement(Attributes = "onsubmit")]
|
||||
public class CSPEventTagHelper : TagHelper
|
||||
{
|
||||
public const string EventNames = "onclick,onkeypress,onchange,onsubmit";
|
||||
private readonly ContentSecurityPolicies _csp;
|
||||
|
||||
readonly static HashSet<string> EventSet = EventNames.Split(',')
|
||||
.ToHashSet();
|
||||
public CSPEventTagHelper(ContentSecurityPolicies csp)
|
||||
{
|
||||
_csp = csp;
|
||||
}
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
foreach (var attr in output.Attributes)
|
||||
{
|
||||
var n = attr.Name.ToLowerInvariant();
|
||||
if (EventSet.Contains(n))
|
||||
{
|
||||
_csp.AllowUnsafeHashes(attr.Value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Add sha256- to allow inline event handlers in CSP
|
||||
/// </summary>
|
||||
[HtmlTargetElement("template", Attributes = "csp-allow")]
|
||||
public class CSPTemplate : TagHelper
|
||||
{
|
||||
private readonly ContentSecurityPolicies _csp;
|
||||
public CSPTemplate(ContentSecurityPolicies csp)
|
||||
{
|
||||
_csp = csp;
|
||||
}
|
||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
output.Attributes.RemoveAll("csp-allow");
|
||||
var childContent = await output.GetChildContentAsync();
|
||||
var content = childContent.GetContent();
|
||||
_csp.AllowInline(content);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add sha256- to allow inline event handlers in a:href=javascript:
|
||||
/// </summary>
|
||||
[HtmlTargetElement("a", Attributes = "csp-allow")]
|
||||
public class CSPA : TagHelper
|
||||
{
|
||||
private readonly ContentSecurityPolicies _csp;
|
||||
public CSPA(ContentSecurityPolicies csp)
|
||||
{
|
||||
_csp = csp;
|
||||
}
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
output.Attributes.RemoveAll("csp-allow");
|
||||
if (output.Attributes.TryGetAttribute("href", out var attr))
|
||||
{
|
||||
var v = attr.Value.ToString();
|
||||
if (v.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_csp.AllowInline(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -340,8 +340,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
return this.errors.length === 0;
|
||||
},
|
||||
saveEditingItem: function(){
|
||||
if(!this.editingItem.id){
|
||||
this.editingItem.id = this.editingItem.title.toLowerCase().trim();
|
||||
const fallbackId = this.editingItem.title.toLowerCase().trim();
|
||||
if(!this.editingItem.id && fallbackId){
|
||||
this.editingItem.id = fallbackId;
|
||||
this.$nextTick(this.saveEditingItem.bind(this));
|
||||
return;
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
@inject BTCPayServer.Security.ContentSecurityPolicies csp
|
||||
@{
|
||||
Layout = null;
|
||||
csp.Add("script-src", "https://cdn.jsdelivr.net");
|
||||
csp.Add("worker-src", "blob:");
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@ -25,6 +28,6 @@
|
||||
@*Ignore this, this is for making the test ClickOnAllSideMenus happy*@
|
||||
<div class="navbar-brand" style="visibility:collapse;"></div>
|
||||
<redoc spec-url="@Url.ActionLink("Swagger")"></redoc>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.45/bundles/redoc.standalone.js" integrity="sha384-RC31+q3tyqdcilXYaU++ii/FAByqeZ+sjKUHMJ8hMzIY5k4kzNqi4Ett88EZ/4lq" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.54/bundles/redoc.standalone.js" integrity="sha384-pxWFJkxrlfignEDb+sJ8XrdnJQ+V2bsiRqgPnfmOk1i3KKSubbydbolVZJeKisNY" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,41 +1,41 @@
|
||||
@model (Dictionary<string, object> Items, int Level)
|
||||
|
||||
@functions{
|
||||
void DisplayValue(object value)
|
||||
{
|
||||
if (value is string str && str.StartsWith("http"))
|
||||
{
|
||||
<a href="@str" target="_blank" rel="noreferrer noopener">@str</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<table class="table table-sm table-responsive-md removetopborder">
|
||||
@foreach (var (key, value) in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
@if (value is string)
|
||||
@if (value is string str)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
<th class="w-150px">@key</th>
|
||||
}
|
||||
<td>
|
||||
@{ DisplayValue(value); }
|
||||
@if (str.StartsWith("http"))
|
||||
{
|
||||
<a href="@str" target="_blank" rel="noreferrer noopener">@str</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@value
|
||||
}
|
||||
</td>
|
||||
}
|
||||
else if (value is Dictionary<string, object>subItems)
|
||||
{
|
||||
@* This is the array case *@
|
||||
if (subItems.Count == 1 && subItems.First().Value is string)
|
||||
if (subItems.Count == 1 && subItems.First().Value is string str2)
|
||||
{
|
||||
<th class="w-150px">@key</th>
|
||||
<td>
|
||||
@{ DisplayValue(subItems.First().Value); }
|
||||
@if (str2.StartsWith("http"))
|
||||
{
|
||||
<a href="@str2" target="_blank" rel="noreferrer noopener">@str2</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@subItems.First().Value
|
||||
}
|
||||
</td>
|
||||
}
|
||||
else
|
||||
|
@ -64,7 +64,7 @@
|
||||
@if (Model.PermissionValues[i].StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
|
||||
{
|
||||
<div class="list-group-item form-group">
|
||||
<div class="form-check">
|
||||
<div class="form-check d-flex">
|
||||
@if (Model.Strict || Model.PermissionValues[i].Forbidden)
|
||||
{
|
||||
<input id="@Model.PermissionValues[i].Permission" type="hidden" asp-for="PermissionValues[i].Value"/>
|
||||
@ -74,10 +74,10 @@
|
||||
{
|
||||
<input id="@Model.PermissionValues[i].Permission" type="checkbox" asp-for="PermissionValues[i].Value" class="form-check-input"/>
|
||||
}
|
||||
<label for="@Model.PermissionValues[i].Permission" class="h5 form-check-label">@Model.PermissionValues[i].Title</label>
|
||||
<label for="@Model.PermissionValues[i].Permission" class="h5 form-check-label m-0 me-4 ms-2">@Model.PermissionValues[i].Title</label>
|
||||
@if (Model.SelectiveStores)
|
||||
{
|
||||
<button type="submit" class="btn btn-link" name="command" value="@($"{Model.PermissionValues[i].Permission}:change-store-mode")">select specific stores...</button>
|
||||
<button type="submit" class="btn btn-link p-0 me-4" name="command" value="@($"{Model.PermissionValues[i].Permission}:change-store-mode")">select specific stores...</button>
|
||||
}
|
||||
|
||||
@if (Model.PermissionValues[i].Forbidden)
|
||||
|
@ -5,7 +5,10 @@
|
||||
@{
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Plugins);
|
||||
var installed = Model.Installed.ToDictionary(plugin => plugin.Identifier.ToLowerInvariant(), plugin => plugin.Version);
|
||||
var availableAndNotInstalled = Model.Available.Where(plugin => !installed.ContainsKey(plugin.Identifier.ToLowerInvariant())).Select(plugin => (plugin, BTCPayServerOptions.RecommendedPlugins.Contains(plugin.Identifier.ToLowerInvariant()))).OrderBy(tuple => tuple.Item1);
|
||||
var availableAndNotInstalled = Model.Available
|
||||
.Where(plugin => !installed.ContainsKey(plugin.Identifier.ToLowerInvariant()))
|
||||
.OrderBy(plugin => plugin.Identifier)
|
||||
.ToList();
|
||||
|
||||
bool DependentOn(string plugin)
|
||||
{
|
||||
@ -236,16 +239,17 @@
|
||||
{
|
||||
<h2 class="mb-4">Available Plugins</h2>
|
||||
<div class="row mb-4">
|
||||
@foreach (var pluginT in availableAndNotInstalled)
|
||||
@foreach (var plugin in availableAndNotInstalled)
|
||||
{
|
||||
var plugin = pluginT.Item1;
|
||||
var recommended = BTCPayServerOptions.RecommendedPlugins.Contains(plugin.Identifier.ToLowerInvariant());
|
||||
|
||||
<div class="col col-12 col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title d-inline-block" data-bs-toggle="tooltip" title="@plugin.Identifier">@plugin.Name</h4>
|
||||
<h5 class="card-subtitle mb-3 text-muted d-flex align-items-center">
|
||||
@plugin.Version
|
||||
@if (pluginT.Item2)
|
||||
@if (recommended)
|
||||
{
|
||||
<div class="badge bg-light ms-2 text-nowrap" data-bs-toggle="tooltip" title="This plugin has been recommended to be installed by your deployment method.">Recommended <span class="fa fa-question-circle-o text-secondary"></span></div>
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
@inject RoleManager<IdentityRole> RoleManager
|
||||
@inject BTCPayServer.Services.BTCPayServerEnvironment Env
|
||||
@inject ISettingsRepository SettingsRepository
|
||||
@inject LinkGenerator linkGenerator
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"@(Env.IsDeveloping ? " data-devenv" : "")>
|
||||
@ -122,9 +123,19 @@
|
||||
}
|
||||
|
||||
<partial name="LayoutFoot" />
|
||||
|
||||
|
||||
@await RenderSectionAsync("PageFootContent", false)
|
||||
<partial name="LayoutPartials/SyncModal" />
|
||||
|
||||
@{
|
||||
var notificationDisabled = (await SettingsRepository.GetPolicies()).DisableInstantNotifications;
|
||||
if (!notificationDisabled)
|
||||
{
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
notificationDisabled = user?.DisabledNotifications == "all";
|
||||
}
|
||||
}
|
||||
|
||||
<script type="text/javascript">
|
||||
const expectedDomain = @Safe.Json(Env.ExpectedHost);
|
||||
const expectedProtocol = @Safe.Json(Env.ExpectedProtocol);
|
||||
@ -133,5 +144,38 @@
|
||||
document.getElementById("browserScheme").innerText = window.location.protocol.substr(0, window.location.protocol.length -1);
|
||||
}
|
||||
</script>
|
||||
@if (!notificationDisabled)
|
||||
{
|
||||
<script>
|
||||
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
|
||||
if (supportsWebSockets) {
|
||||
|
||||
var loc = window.location, ws_uri;
|
||||
if (loc.protocol === "https:") {
|
||||
ws_uri = "wss:";
|
||||
} else {
|
||||
ws_uri = "ws:";
|
||||
}
|
||||
ws_uri += "//" + loc.host;
|
||||
ws_uri += "@linkGenerator.GetPathByAction("SubscribeUpdates", "Notifications")";
|
||||
var newDataEndpoint = "@linkGenerator.GetPathByAction("GetNotificationDropdownUI", "Notifications")";
|
||||
|
||||
try {
|
||||
socket = new WebSocket(ws_uri);
|
||||
socket.onmessage = function (e) {
|
||||
$.get(newDataEndpoint, function (data) {
|
||||
$("#notifications-nav-item").replaceWith($(data));
|
||||
});
|
||||
};
|
||||
socket.onerror = function (e) {
|
||||
console.error("Error while connecting to websocket for notifications (callback)", e);
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error("Error while connecting to websocket for notifications", e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,7 +1,9 @@
|
||||
@inject BTCPayServer.Security.ContentSecurityPolicies csp
|
||||
@model PayButtonViewModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePageAndTitle(StoreNavPages.PayButton, "Pay Button", Context.GetStoreData().StoreName);
|
||||
csp.AllowUnsafeHashes("onBTCPayFormSubmit(event);return false");
|
||||
}
|
||||
|
||||
@section PageHeadContent {
|
||||
@ -14,6 +16,24 @@
|
||||
<script src="~/vendor/vuejs-vee-validate/vee-validate.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/clipboard.js/clipboard.js" asp-append-version="true"></script>
|
||||
<script src="~/paybutton/paybutton.js" asp-append-version="true"></script>
|
||||
<template id="template-get-scripts" csp-allow>
|
||||
if (!window.btcpay) {
|
||||
var script = document.createElement('script');
|
||||
script.src=@(Safe.Json(Model.UrlRoot + "modal/btcpay.js"));
|
||||
document.getElementsByTagName('head')[0].append(script);
|
||||
}
|
||||
function onBTCPayFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200 && this.responseText) {
|
||||
window.btcpay.showInvoice(JSON.parse(this.responseText).invoiceId);
|
||||
}
|
||||
};
|
||||
xhttp.open('POST', event.target.getAttribute('action'), true);
|
||||
xhttp.send(new FormData(event.target));
|
||||
}
|
||||
</template>
|
||||
<script>
|
||||
var srvModel = @Safe.Json(Model);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
hljs.initHighlightingOnLoad();
|
||||
$(document).ready(function () {
|
||||
$(".richtext").summernote(window.summernoteOptions || {});
|
||||
$(".richtext").summernote(window.summernoteOptions());
|
||||
});
|
||||
|
@ -1,8 +1,12 @@
|
||||
window.summernoteOptions = {
|
||||
minHeight: 300,
|
||||
tableClassName: 'table table-sm',
|
||||
insertTableMaxSize: {
|
||||
col: 5,
|
||||
row: 10
|
||||
window.summernoteOptions = function() {
|
||||
return {
|
||||
minHeight: 300,
|
||||
tableClassName: 'table table-sm',
|
||||
insertTableMaxSize: {
|
||||
col: 5,
|
||||
row: 10
|
||||
},
|
||||
codeviewFilter: true,
|
||||
codeviewFilterRegex: new RegExp($.summernote.options.codeviewFilterRegex.source + '|<.*?( on\\w+?=.*?)>', 'gi')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -42,33 +42,11 @@ function getStyles (styles) {
|
||||
}
|
||||
|
||||
function getScripts(srvModel) {
|
||||
return ""+
|
||||
"<script>" +
|
||||
"if(!window.btcpay){ " +
|
||||
" var head = document.getElementsByTagName('head')[0];" +
|
||||
" var script = document.createElement('script');" +
|
||||
" script.src='"+esc(srvModel.urlRoot)+"modal/btcpay.js';" +
|
||||
" script.type = 'text/javascript';" +
|
||||
" head.append(script);" +
|
||||
"}" +
|
||||
"function onBTCPayFormSubmit(event){" +
|
||||
" var xhttp = new XMLHttpRequest();" +
|
||||
" xhttp.onreadystatechange = function() {" +
|
||||
" if (this.readyState == 4 && this.status == 200) {" +
|
||||
" if(this.status == 200 && this.responseText){" +
|
||||
" var response = JSON.parse(this.responseText);" +
|
||||
" window.btcpay.showInvoice(response.invoiceId);" +
|
||||
" }" +
|
||||
" }" +
|
||||
" };" +
|
||||
" xhttp.open(\"POST\", event.target.getAttribute('action'), true);" +
|
||||
" xhttp.send(new FormData( event.target ));" +
|
||||
"}" +
|
||||
"</script>";
|
||||
if (!srvModel.useModal) return ''
|
||||
const template = document.getElementById('template-get-scripts')
|
||||
return template.innerHTML.replace(/&/g, '&')
|
||||
}
|
||||
|
||||
|
||||
|
||||
function inputChanges(event, buttonSize) {
|
||||
if (buttonSize !== null && buttonSize !== undefined) {
|
||||
srvModel.buttonSize = buttonSize;
|
||||
@ -115,12 +93,10 @@ function inputChanges(event, buttonSize) {
|
||||
}
|
||||
|
||||
var html =
|
||||
//Scripts
|
||||
(srvModel.useModal? getScripts(srvModel) :"") +
|
||||
// Styles
|
||||
getStyles('template-paybutton-styles') + (isSlider ? getStyles('template-slider-styles') : '') +
|
||||
// Form
|
||||
'<form method="POST" '+ ( srvModel.useModal? ' onsubmit="onBTCPayFormSubmit(event);return false" ' : '' )+' action="' + esc(srvModel.urlRoot) + actionUrl + '" class="btcpay-form btcpay-form--' + (srvModel.fitButtonInline ? 'inline' : 'block') +'">\n' +
|
||||
'<form method="POST"' + (srvModel.useModal ? ' onsubmit="onBTCPayFormSubmit(event);return false"' : '') + ' action="' + esc(srvModel.urlRoot) + actionUrl + '" class="btcpay-form btcpay-form--' + (srvModel.fitButtonInline ? 'inline' : 'block') +'">\n' +
|
||||
addInput("storeId", srvModel.storeId);
|
||||
|
||||
if(app){
|
||||
@ -144,7 +120,6 @@ function inputChanges(event, buttonSize) {
|
||||
|
||||
if (srvModel.checkoutQueryString) html += addInput("checkoutQueryString", srvModel.checkoutQueryString);
|
||||
}
|
||||
|
||||
|
||||
// Fixed amount: Add price and currency as hidden inputs
|
||||
if (isFixedAmount) {
|
||||
@ -192,10 +167,21 @@ function inputChanges(event, buttonSize) {
|
||||
'</button>'
|
||||
}
|
||||
html += '</form>';
|
||||
|
||||
// Scripts
|
||||
var scripts = getScripts(srvModel);
|
||||
var code = html + (scripts ? `\n<script>\n ${scripts.trim()}\n</script>` : '')
|
||||
|
||||
$("#mainCode").text(html).html();
|
||||
$("#preview").html(html);
|
||||
var form = document.querySelector("#preview form");
|
||||
$("#mainCode").text(code).html();
|
||||
var preview = document.getElementById('preview');
|
||||
preview.innerHTML = html;
|
||||
if (scripts) {
|
||||
// script needs to be inserted as node, otherwise it won't get executed
|
||||
var script = document.createElement('script');
|
||||
script.innerHTML = scripts
|
||||
preview.appendChild(script)
|
||||
}
|
||||
var form = preview.querySelector("form");
|
||||
var url = new URL(form.getAttribute("action"));
|
||||
var formData = new FormData(form);
|
||||
formData.forEach((value, key) => {
|
||||
|
@ -1,3 +1,3 @@
|
||||
$(document).ready(function() {
|
||||
$(".richtext").summernote(window.summernoteOptions || {});
|
||||
$(".richtext").summernote(window.summernoteOptions());
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
hljs.initHighlightingOnLoad();
|
||||
$(document).ready(function () {
|
||||
$(".richtext").summernote(window.summernoteOptions || {});
|
||||
$(".richtext").summernote(window.summernoteOptions());
|
||||
});
|
||||
|
@ -5366,6 +5366,9 @@ var Editor_Editor = /*#__PURE__*/function () {
|
||||
// if url doesn't have any protocol and not even a relative or a label, use http:// as default
|
||||
linkUrl = /^([A-Za-z][A-Za-z0-9+-.]*\:|#|\/)/.test(linkUrl) ? linkUrl : _this.options.defaultProtocol + linkUrl;
|
||||
}
|
||||
|
||||
linkUrl = linkUrl.replace(this.options.codeviewFilterRegex, '');
|
||||
linkText = linkText.replace(this.options.codeviewFilterRegex, '');
|
||||
|
||||
var anchors = [];
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.2.2</Version>
|
||||
<Version>1.2.4</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
26
Changelog.md
26
Changelog.md
@ -1,5 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
## 1.2.4
|
||||
|
||||
Minor bug fixes release, update recommended for shared hosting.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* If `Only enable the payment method after user explicitly chooses it` is enabled for a store and a payment method is unavailable, the server could become unresponsive. @NicolasDorier
|
||||
* Authorize API key page was broken when trying to select specific stores (#2858) @ubolator
|
||||
* The /docs page was broken in 1.2.3 due to CSP @NicolasDorier
|
||||
* Fixing crashes happening when someone migrate from BTCPay Server altcoins edition back to bitcoin only @Kukks
|
||||
|
||||
## 1.2.3
|
||||
|
||||
This release fixes three XSS vulnerabilities. Those vulnerabilities only impacts shared BTCPay instances.
|
||||
Special thanks to Ajmal "@b3ef" Aboobacker and Abdul "@b1nslashsh" muhaimin for finding them who contacted us through @huntrdev.
|
||||
See [1](https://huntr.dev/bounties/ffabdac8-7280-4806-b70c-9b0d1aafbb6e/), [2](https://www.huntr.dev/bounties/32e30ecf-31fa-45f6-8552-47250ef0e613/) and [3](https://huntr.dev/bounties/0fcdee5f-1f07-47ce-b650-ea8b4a7d35d8/).
|
||||
|
||||
### Bug fixes:
|
||||
|
||||
* Use CSP to prevent future XSS vulnerabilities. (#2856, #2863) @NicolasDorier
|
||||
* Fix XSS vulnerabilities in summernote, the rich text editor (#2859) @dennisreimann
|
||||
* Fix plugins page crashing @Kukks
|
||||
* Fix page crash of the perk editor in the crowdfund settings when the title is not set @dennisreimann
|
||||
* Do not generate payment methods when 0 amount invoice (#2776)
|
||||
* When using the BTCPay Vault, some hardware wallet types were considered unknown @NicolasDorier
|
||||
|
||||
## 1.2.2
|
||||
|
||||
# Bug fixes:
|
||||
|
Reference in New Issue
Block a user