Compare commits

...

10 Commits

Author SHA1 Message Date
c804f55d82 bump 2017-10-17 11:19:57 +09:00
c96a25c9b9 Checkout page was not working correctly on /invoice?id= 2017-10-17 11:16:34 +09:00
4c57726be7 bump 2017-10-15 15:33:15 +09:00
8f723d7131 Fix typos 2017-10-15 15:32:53 +09:00
a7e10c0fb9 Can't pair same SIN to different store 2017-10-13 18:06:46 +09:00
15e73e1cad Properly limit CORS to bitpay api 2017-10-13 17:46:19 +09:00
a17192ee99 Add Cors 2017-10-13 17:18:32 +09:00
27200d1fb0 X-Frame-Options 2017-10-13 17:13:21 +09:00
9ddceae824 Validate email in the api 2017-10-13 16:59:02 +09:00
d1961e0938 Support other way of passing buyer info 2017-10-13 16:44:55 +09:00
14 changed files with 159 additions and 34 deletions

View File

@ -15,6 +15,7 @@ using System.IO;
using Newtonsoft.Json.Linq;
using BTCPayServer.Controllers;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Authentication;
namespace BTCPayServer.Tests
{
@ -69,6 +70,7 @@ namespace BTCPayServer.Tests
user.GrantAccess();
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 5000.0,
Currency = "USD",
PosData = "posData",
@ -167,6 +169,25 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CantPairTwiceWithSamePubkey()
{
using(var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
acc.Register();
var store = acc.CreateStore();
var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<RedirectToActionResult>(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter().GetResult());
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
var store2 = acc.CreateStore();
store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult();
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage);
}
}
[Fact]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{

View File

@ -13,6 +13,14 @@ using System.Linq;
namespace BTCPayServer.Authentication
{
public enum PairingResult
{
Partial,
Complete,
ReusedKey,
Expired
}
public class TokenRepository
{
ApplicationDbContextFactory _Factory;
@ -79,40 +87,45 @@ namespace BTCPayServer.Authentication
}
}
public async Task<bool> PairWithStoreAsync(string pairingCodeId, string storeId)
public async Task<PairingResult> PairWithStoreAsync(string pairingCodeId, string storeId)
{
using(var ctx = _Factory.CreateContext())
{
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
return false;
return PairingResult.Expired;
pairingCode.StoreDataId = storeId;
await ActivateIfComplete(ctx, pairingCode);
var result = await ActivateIfComplete(ctx, pairingCode);
await ctx.SaveChangesAsync();
return result;
}
return true;
}
public async Task<bool> PairWithSINAsync(string pairingCodeId, string sin)
public async Task<PairingResult> PairWithSINAsync(string pairingCodeId, string sin)
{
using(var ctx = _Factory.CreateContext())
{
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
return false;
return PairingResult.Expired;
pairingCode.SIN = sin;
await ActivateIfComplete(ctx, pairingCode);
var result = await ActivateIfComplete(ctx, pairingCode);
await ctx.SaveChangesAsync();
return result;
}
return true;
}
private async Task ActivateIfComplete(ApplicationDbContext ctx, PairingCodeData pairingCode)
private async Task<PairingResult> ActivateIfComplete(ApplicationDbContext ctx, PairingCodeData pairingCode)
{
if(!string.IsNullOrEmpty(pairingCode.SIN) && !string.IsNullOrEmpty(pairingCode.StoreDataId))
{
ctx.PairingCodes.Remove(pairingCode);
// Can have concurrency issues... but no harm can be done
var alreadyUsed = await ctx.PairedSINData.Where(p => p.SIN == pairingCode.SIN && p.StoreDataId != pairingCode.StoreDataId).AnyAsync();
if(alreadyUsed)
return PairingResult.ReusedKey;
await ctx.PairedSINData.AddAsync(new PairedSINData()
{
Id = pairingCode.TokenValue,
@ -122,7 +135,9 @@ namespace BTCPayServer.Authentication
StoreDataId = pairingCode.StoreDataId,
SIN = pairingCode.SIN
});
return PairingResult.Complete;
}
return PairingResult.Partial;
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.11</Version>
<Version>1.0.0.14</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
@ -22,7 +22,7 @@
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="NBitcoin" Version="4.0.0.38" />
<PackageReference Include="NBitpayClient" Version="1.0.0.10" />
<PackageReference Include="NBitpayClient" Version="1.0.0.11" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.17" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />

View File

@ -57,8 +57,10 @@ namespace BTCPayServer.Controllers
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
pairingEntity.SIN = sin;
if(!await _TokenRepository.PairWithSINAsync(request.PairingCode, sin))
throw new BitpayHttpException(400, "Unknown pairing code");
var result = await _TokenRepository.PairWithSINAsync(request.PairingCode, sin);
if(result != PairingResult.Complete && result != PairingResult.Partial)
throw new BitpayHttpException(400, $"Error while pairing ({result})");
}

View File

@ -11,25 +11,43 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Servcices.Invoices;
using Microsoft.AspNetCore.Cors;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController
[EnableCors("BitpayAPI")]
[BitpayAPIConstraint]
public class InvoiceControllerAPI : Controller
{
private InvoiceController _InvoiceController;
private InvoiceRepository _InvoiceRepository;
private TokenRepository _TokenRepository;
private StoreRepository _StoreRepository;
public InvoiceControllerAPI(InvoiceController invoiceController,
InvoiceRepository invoceRepository,
TokenRepository tokenRepository,
StoreRepository storeRepository)
{
this._InvoiceController = invoiceController;
this._InvoiceRepository = invoceRepository;
this._TokenRepository = tokenRepository;
this._StoreRepository = storeRepository;
}
[HttpPost]
[Route("invoices")]
[MediaTypeConstraint("application/json")]
[BitpayAPIConstraint]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
{
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
var store = await FindStore(bitToken);
return await CreateInvoiceCore(invoice, store);
return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot());
}
[HttpGet]
[Route("invoices/{id}")]
[BitpayAPIConstraint]
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
{
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
@ -44,7 +62,6 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("invoices")]
[BitpayAPIConstraint]
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
string token,
DateTimeOffset? dateStart = null,

View File

@ -3,6 +3,7 @@ using BTCPayServer.Filters;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Servcices.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
@ -81,7 +82,7 @@ namespace BTCPayServer.Controllers
m.Confirmations = (await _Explorer.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
m.TransactionId = payment.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = _Network == Network.Main ? $"https://www.smartbit.com.au/tx/{m.TransactionId}" : $"https://testnet.smartbit.com.au/{m.TransactionId}";
m.TransactionLink = _Network == Network.Main ? $"https://www.smartbit.com.au/tx/{m.TransactionId}" : $"https://testnet.smartbit.com.au/tx/{m.TransactionId}";
return m;
})
.ToArray();
@ -95,6 +96,7 @@ namespace BTCPayServer.Controllers
[Route("i/{invoiceId}")]
[Route("invoice")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)]
public async Task<IActionResult> Checkout(string invoiceId, string id = null)
{
//Keep compatibility with Bitpay
@ -244,7 +246,7 @@ namespace BTCPayServer.Controllers
ItemDesc = model.ItemDesc,
FullNotifications = true,
BuyerEmail = model.BuyerEmail,
}, store);
}, store, HttpContext.Request.GetAbsoluteRoot());
StatusMessage = $"Invoice {result.Data.Id} just created!";
return RedirectToAction(nameof(ListInvoices));

View File

@ -43,7 +43,6 @@ namespace BTCPayServer.Controllers
{
public partial class InvoiceController : Controller
{
TokenRepository _TokenRepository;
InvoiceRepository _InvoiceRepository;
BTCPayWallet _Wallet;
IRateProvider _RateProvider;
@ -58,7 +57,6 @@ namespace BTCPayServer.Controllers
Network network,
InvoiceRepository invoiceRepository,
UserManager<ApplicationUser> userManager,
TokenRepository tokenRepository,
BTCPayWallet wallet,
IRateProvider rateProvider,
StoreRepository storeRepository,
@ -69,7 +67,6 @@ namespace BTCPayServer.Controllers
_Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_Network = network ?? throw new ArgumentNullException(nameof(network));
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
@ -78,7 +75,7 @@ namespace BTCPayServer.Controllers
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
}
private async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store)
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
{
var derivationStrategy = store.DerivationStrategy;
var entity = new InvoiceEntity
@ -91,11 +88,18 @@ namespace BTCPayServer.Controllers
notificationUri = null;
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
entity.ExpirationTime = entity.InvoiceTime + TimeSpan.FromMinutes(15.0);
entity.ServerUrl = HttpContext.Request.GetAbsoluteRoot();
entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
entity.RefundMail = EmailValidator.IsEmail(entity?.BuyerInformation?.BuyerEmail) ? entity.BuyerInformation.BuyerEmail : null;
//Another way of passing buyer info to support
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
if(entity?.BuyerInformation?.BuyerEmail != null)
{
if(!EmailValidator.IsEmail(entity.BuyerInformation.BuyerEmail))
throw new BitpayHttpException(400, "Invalid email");
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
}
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
entity.Status = "new";
@ -112,6 +116,21 @@ namespace BTCPayServer.Controllers
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
private void FillBuyerInfo(Buyer buyer, BuyerInformation buyerInformation)
{
if(buyer == null)
return;
buyerInformation.BuyerAddress1 = buyerInformation.BuyerAddress1 ?? buyer.Address1;
buyerInformation.BuyerAddress2 = buyerInformation.BuyerAddress2 ?? buyer.Address2;
buyerInformation.BuyerCity = buyerInformation.BuyerCity ?? buyer.City;
buyerInformation.BuyerCountry = buyerInformation.BuyerCountry ?? buyer.country;
buyerInformation.BuyerEmail = buyerInformation.BuyerEmail ?? buyer.email;
buyerInformation.BuyerName = buyerInformation.BuyerName ?? buyer.Name;
buyerInformation.BuyerPhone = buyerInformation.BuyerPhone ?? buyer.phone;
buyerInformation.BuyerState = buyerInformation.BuyerState ?? buyer.State;
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy)
{
return new DerivationStrategyFactory(_Network).Parse(derivationStrategy);

View File

@ -320,14 +320,18 @@ namespace BTCPayServer.Controllers
[Route("api-access-request")]
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
{
if(pairingCode == null)
return NotFound();
var store = await _Repo.FindStore(selectedStore, GetUserId());
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
if(store == null || pairing == null)
return NotFound();
if(pairingCode != null && await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id))
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
if(pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
{
StatusMessage = "Pairing is successfull";
if(pairing.SIN == null)
if(pairingResult == PairingResult.Partial)
StatusMessage = "Server initiated pairing code: " + pairingCode;
return RedirectToAction(nameof(ListTokens), new
{
@ -336,7 +340,7 @@ namespace BTCPayServer.Controllers
}
else
{
StatusMessage = "Pairing failed";
StatusMessage = $"Pairing failed ({pairingResult})";
return RedirectToAction(nameof(ListTokens), new
{
storeId = store.Id

View File

@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Filters
{
public class XFrameOptionsAttribute : Attribute, IActionFilter
{
public XFrameOptionsAttribute(string value)
{
Value = value;
}
public string Value
{
get; set;
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
var existing = context.HttpContext.Response.Headers["x-frame-options"].FirstOrDefault();
if(existing != null && Value == null)
context.HttpContext.Response.Headers.Remove("x-frame-options");
else
context.HttpContext.Response.Headers["x-frame-options"] = Value;
}
}
}

View File

@ -141,6 +141,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
services.AddTransient<AccessTokenController>();
services.AddTransient<CallbackController>();
services.AddTransient<InvoiceController>();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();

View File

@ -35,6 +35,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Threading;
using Microsoft.Extensions.Options;
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
using Microsoft.AspNetCore.Mvc.Cors.Internal;
namespace BTCPayServer.Hosting
{
@ -74,7 +75,10 @@ namespace BTCPayServer.Hosting
// Big hack, tests fails because Hangfire fail at initializing at the second test run
AddHangfireFix(services);
services.AddBTCPayServer();
services.AddMvc();
services.AddMvc(o =>
{
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
});
}
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run
@ -103,6 +107,13 @@ namespace BTCPayServer.Hosting
}));
services.AddHangfire(configuration);
services.AddCors(o =>
{
o.AddPolicy("BitpayAPI", b =>
{
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
{
@ -131,7 +142,6 @@ namespace BTCPayServer.Hosting
app.UsePayServer();
app.UseStaticFiles();
app.UseAuthentication();
app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions() { Authorization = new[] { new NeedRole(Roles.ServerAdmin) } });
app.UseMvc(routes =>

View File

@ -27,6 +27,7 @@
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.qrcode/1.0/jquery.qrcode.min.js"></script>
<script type="text/javascript">
var invoiceId = "@Model.InvoiceId";
var btcAddress = "@Model.BTCAddress";
var btcDue = "@Model.BTCDue"; //must be a string
var customerEmail = "@Model.CustomerEmail"; // Place holder

View File

@ -66,7 +66,7 @@
<td>xpub-[p2sh]</td>
</tr>
<tr>
<td>P2SH</td>
<td>P2PKH</td>
<td>xpub-[legacy]</td>
</tr>
<tr>

View File

@ -69,7 +69,7 @@ function emailForm() {
// Push the email to a server, once the reception is confirmed move on
customerEmail = emailAddress;
var path = window.location.pathname + "/UpdateCustomer";
var path = "i/" + invoiceId + "/UpdateCustomer";
$.ajax({
url: path,
@ -186,7 +186,7 @@ function updateState(status) {
}
var watcher = setInterval(function () {
var path = window.location.pathname + "/status";
var path = "i/" + invoiceId + "/status";
$.ajax({
url: path,
type: "GET"