Simplify vault logic by introducing a VaultClient (#5434)

This commit is contained in:
Nicolas Dorier 2023-10-27 11:54:15 +09:00 committed by GitHub
parent 89041a6744
commit b702621a04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 227 additions and 76 deletions

View File

@ -50,9 +50,10 @@ namespace BTCPayServer.Controllers
if (network == null)
return NotFound();
var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var vaultClient = new VaultClient(websocket);
var hwi = new Hwi.HwiClient(network.NBitcoinNetwork)
{
Transport = new HwiWebSocketTransport(websocket)
Transport = new VaultHWITransport(vaultClient)
};
Hwi.HwiDeviceClient device = null;
HwiEnumerateEntry deviceEntry = null;

View File

@ -1,24 +0,0 @@
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace BTCPayServer
{
public class HwiWebSocketTransport : Hwi.Transports.ITransport
{
private readonly WebSocketHelper _webSocket;
public HwiWebSocketTransport(WebSocket webSocket)
{
_webSocket = new WebSocketHelper(webSocket);
}
public async Task<string> SendCommandAsync(string[] arguments, CancellationToken cancel)
{
JObject request = new JObject();
request.Add("params", new JArray(arguments));
await _webSocket.Send(request.ToString(), cancel);
return await _webSocket.NextMessageAsync(cancel);
}
}
}

129
BTCPayServer/VaultClient.cs Normal file
View File

@ -0,0 +1,129 @@
#nullable enable
using System;
using System.Net.WebSockets;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Amazon.S3.Model.Internal.MarshallTransformations;
using ExchangeSharp;
using Newtonsoft.Json.Linq;
namespace BTCPayServer
{
public enum VaultMessageType
{
Ok,
Error,
Processing
}
public enum VaultServices
{
HWI,
NFC
}
public class VaultNotConnectedException : VaultException
{
public VaultNotConnectedException() : base("BTCPay Vault isn't connected")
{
}
}
public class VaultException : Exception
{
public VaultException(string message) : base(message)
{
}
}
public class VaultClient
{
public VaultClient(WebSocket websocket)
{
Websocket = new WebSocketHelper(websocket);
}
public WebSocketHelper Websocket { get; }
public async Task<string> GetNextCommand(CancellationToken cancellationToken)
{
return await Websocket.NextMessageAsync(cancellationToken);
}
public async Task SendMessage(JObject mess, CancellationToken cancellationToken)
{
await Websocket.Send(mess.ToString(), cancellationToken);
}
public Task Show(VaultMessageType type, string message, CancellationToken cancellationToken)
{
return Show(type, message, null, cancellationToken);
}
public async Task Show(VaultMessageType type, string message, string? debug, CancellationToken cancellationToken)
{
await SendMessage(new JObject()
{
["command"] = "showMessage",
["message"] = message,
["type"] = type.ToString(),
["debug"] = debug
}, cancellationToken);
}
string? _ServiceUri;
public async Task<bool?> AskPermission(VaultServices service, CancellationToken cancellationToken)
{
var uri = service switch
{
VaultServices.HWI => "http://127.0.0.1:65092/hwi-bridge/v1",
VaultServices.NFC => "http://127.0.0.1:65092/nfc-bridge/v1",
_ => throw new NotSupportedException()
};
await this.SendMessage(new JObject()
{
["command"] = "sendRequest",
["uri"] = uri + "/request-permission"
}, cancellationToken);
var result = await GetNextMessage(cancellationToken);
if (result["httpCode"] is { } p)
{
var ok = p.Value<int>() == 200;
if (ok)
_ServiceUri = uri;
return ok;
}
return null;
}
public async Task<JToken?> SendVaultRequest(string? path, JObject? body, CancellationToken cancellationToken)
{
var isAbsolute = path is not null && Uri.IsWellFormedUriString(path, UriKind.Absolute);
var query = new JObject()
{
["command"] = "sendRequest",
["uri"] = isAbsolute ? path : _ServiceUri + path
};
if (body is not null)
query["body"] = body;
await this.SendMessage(query, cancellationToken);
var resp = await GetNextMessage(cancellationToken);
if (resp["httpCode"] is not { } p)
throw new VaultNotConnectedException();
if (p.Value<int>() != 200)
throw new VaultException($"Unexpected response code from vault {p.Value<int>()}");
return resp["body"] as JToken;
}
public async Task<JObject> GetNextMessage(CancellationToken cancellationToken)
{
return JObject.Parse(await this.Websocket.NextMessageAsync(cancellationToken));
}
public Task SendSimpleMessage(string command, CancellationToken cancellationToken)
{
return SendMessage(new JObject() { ["command"] = command }, cancellationToken);
}
}
}

View File

@ -0,0 +1,26 @@
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace BTCPayServer
{
public class VaultHWITransport : Hwi.Transports.ITransport
{
private readonly VaultClient _vaultClient;
public VaultHWITransport(VaultClient vaultClient)
{
_vaultClient = vaultClient;
}
public async Task<string> SendCommandAsync(string[] arguments, CancellationToken cancel)
{
var resp = await _vaultClient.SendVaultRequest("http://127.0.0.1:65092/hwi-bridge/v1",
new JObject()
{
["params"] = new JArray(arguments)
}, cancel);
return (string)((JValue)resp).Value;
}
}
}

View File

@ -7,6 +7,12 @@
</div>
<div class="vault-feedback vault-feedback3 mb-2 d-flex">
<span class="vault-feedback-icon mt-1 me-2"></span> <span class="vault-feedback-content flex-grow"></span>
</div>
<div class="vault-feedback vault-feedback4 mb-2 d-flex">
<span class="vault-feedback-icon mt-1 me-2"></span> <span class="vault-feedback-content flex-grow"></span>
</div>
<div class="vault-feedback vault-feedback5 mb-2 d-flex">
<span class="vault-feedback-icon mt-1 me-2"></span> <span class="vault-feedback-content flex-grow"></span>
</div>
<div id="pin-input" class="mt-4" style="display: none;">
<div class="row">

View File

@ -7,8 +7,6 @@ var vault = (function () {
* @type {WebSocket}
*/
this.socket = websocket;
this.onerror = function (error) { };
this.onbackendmessage = function (json) { };
this.close = function () { if (websocket) websocket.close(); };
/**
* @returns {Promise}
@ -23,28 +21,37 @@ var vault = (function () {
if (event.data === "ping")
return;
var jsonObject = JSON.parse(event.data);
if (jsonObject.hasOwnProperty("params")) {
if (jsonObject.command == "sendRequest") {
var request = new XMLHttpRequest();
request.onreadystatechange = function () {
if (request.readyState == 4 && request.status == 200) {
if (self.socket.readyState == 1)
self.socket.send(request.responseText);
if (request.readyState == 4) {
if (request.status === 0) {
self.socket.send("{\"error\": \"Failed to connect to uri\"}");
}
else if (self.socket.readyState == 1) {
var body = null;
if (request.responseText) {
var contentType = request.getResponseHeader('Content-Type') || 'text/plain';
if (contentType === 'text/plain')
body = request.responseText;
else
self.onerror(vault.errors.socketError);
body = JSON.parse(request.responseText);
}
if (request.readyState == 4 && request.status == 0) {
self.onerror(vault.errors.notRunning);
self.socket.send(JSON.stringify(
{
httpCode: request.status,
body: body
}));
}
if (request.readyState == 4 && request.status == 401) {
self.onerror(vault.errors.denied);
}
};
request.overrideMimeType("text/plain");
request.open('POST', 'http://127.0.0.1:65092/hwi-bridge/v1');
request.send(JSON.stringify(jsonObject));
request.open('POST', jsonObject.uri);
jsonObject.body = jsonObject.body || {};
request.send(JSON.stringify(jsonObject.body));
}
else {
self.onbackendmessage(jsonObject);
if (self.nextResolveBackendMessage)
self.nextResolveBackendMessage(jsonObject);
}

View File

@ -6,52 +6,50 @@ var vaultui = (function () {
/**
* @param {string} type
* @param {string} txt
* @param {string} category
* @param {string} id
*/
function VaultFeedback(type, txt, category, id) {
function VaultFeedback(type, txt, id) {
var self = this;
this.type = type;
this.txt = txt;
this.category = category;
this.id = id;
/**
* @param {string} str
* @param {string} by
*/
this.replace = function (str, by) {
return new VaultFeedback(self.type, self.txt.replace(str, by), self.category, self.id);
return new VaultFeedback(self.type, self.txt.replace(str, by), self.id);
};
}
var VaultFeedbacks = {
vaultLoading: new VaultFeedback("?", "Checking BTCPay Server Vault is running...", "vault-feedback1", "vault-loading"),
vaultDenied: new VaultFeedback("failed", "The user declined access to the vault.", "vault-feedback1", "vault-denied"),
vaultGranted: new VaultFeedback("ok", "Access to vault granted by owner.", "vault-feedback1", "vault-granted"),
noVault: new VaultFeedback("failed", "BTCPay Server Vault does not seem to be running, you can download it on <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">Github</a>.", "vault-feedback1", "no-vault"),
noWebsockets: new VaultFeedback("failed", "Web sockets are not supported by the browser.", "vault-feedback1", "no-websocket"),
errorWebsockets: new VaultFeedback("failed", "Error of the websocket while connecting to the backend.", "vault-feedback1", "error-websocket"),
bridgeConnected: new VaultFeedback("ok", "BTCPayServer successfully connected to the vault.", "vault-feedback1", "bridge-connected"),
vaultNeedUpdate: new VaultFeedback("failed", "Your BTCPay Server Vault version is outdated. Please <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">download</a> the latest version.", "vault-feedback2", "vault-outdated"),
noDevice: new VaultFeedback("failed", "No device connected.", "vault-feedback2", "no-device"),
needInitialized: new VaultFeedback("failed", "The device has not been initialized.", "vault-feedback2", "need-initialized"),
fetchingDevice: new VaultFeedback("?", "Fetching device...", "vault-feedback2", "fetching-device"),
deviceFound: new VaultFeedback("ok", "Device found: {{0}}", "vault-feedback2", "device-selected"),
fetchingXpubs: new VaultFeedback("?", "Fetching public keys...", "vault-feedback3", "fetching-xpubs"),
askXpubs: new VaultFeedback("?", "Select your address type and account", "vault-feedback3", "fetching-xpubs"),
fetchedXpubs: new VaultFeedback("ok", "Public keys successfully fetched.", "vault-feedback3", "xpubs-fetched"),
unexpectedError: new VaultFeedback("failed", "An unexpected error happened. ({{0}})", "vault-feedback3", "unknown-error"),
invalidNetwork: new VaultFeedback("failed", "The device is targeting a different chain.", "vault-feedback3", "invalid-network"),
needPin: new VaultFeedback("?", "Enter the pin.", "vault-feedback3", "need-pin"),
incorrectPin: new VaultFeedback("failed", "Incorrect pin code.", "vault-feedback3", "incorrect-pin"),
invalidPasswordConfirmation: new VaultFeedback("failed", "Invalid password confirmation.", "vault-feedback3", "invalid-password-confirm"),
wrongWallet: new VaultFeedback("failed", "This device can't sign the transaction. (Wrong device, wrong passphrase or wrong device fingerprint in your wallet settings)", "vault-feedback3", "wrong-wallet"),
wrongKeyPath: new VaultFeedback("failed", "This device can't sign the transaction. (The wallet keypath in your wallet settings seems incorrect)", "vault-feedback3", "wrong-keypath"),
needPassphrase: new VaultFeedback("?", "Enter the passphrase.", "vault-feedback3", "need-passphrase"),
needPassphraseOnDevice: new VaultFeedback("?", "Please, enter the passphrase on the device.", "vault-feedback3", "need-passphrase-on-device"),
signingTransaction: new VaultFeedback("?", "Please review and confirm the transaction on your device...", "vault-feedback3", "ask-signing"),
reviewAddress: new VaultFeedback("?", "Sending... Please review the address on your device...", "vault-feedback3", "ask-signing"),
signingRejected: new VaultFeedback("failed", "The user refused to sign the transaction", "vault-feedback3", "user-reject"),
vaultLoading: new VaultFeedback("?", "Checking BTCPay Server Vault is running...", "vault-loading"),
vaultDenied: new VaultFeedback("failed", "The user declined access to the vault.", "vault-denied"),
vaultGranted: new VaultFeedback("ok", "Access to vault granted by owner.", "vault-granted"),
noVault: new VaultFeedback("failed", "BTCPay Server Vault does not seem to be running, you can download it on <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">Github</a>.", "no-vault"),
noWebsockets: new VaultFeedback("failed", "Web sockets are not supported by the browser.", "no-websocket"),
errorWebsockets: new VaultFeedback("failed", "Error of the websocket while connecting to the backend.", "error-websocket"),
bridgeConnected: new VaultFeedback("ok", "BTCPayServer successfully connected to the vault.", "bridge-connected"),
vaultNeedUpdate: new VaultFeedback("failed", "Your BTCPay Server Vault version is outdated. Please <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">download</a> the latest version.", "vault-outdated"),
noDevice: new VaultFeedback("failed", "No device connected.", "no-device"),
needInitialized: new VaultFeedback("failed", "The device has not been initialized.", "need-initialized"),
fetchingDevice: new VaultFeedback("?", "Fetching device...", "fetching-device"),
deviceFound: new VaultFeedback("ok", "Device found: {{0}}", "device-selected"),
fetchingXpubs: new VaultFeedback("?", "Fetching public keys...", "fetching-xpubs"),
askXpubs: new VaultFeedback("?", "Select your address type and account", "fetching-xpubs"),
fetchedXpubs: new VaultFeedback("ok", "Public keys successfully fetched.", "xpubs-fetched"),
unexpectedError: new VaultFeedback("failed", "An unexpected error happened. ({{0}})", "unknown-error"),
invalidNetwork: new VaultFeedback("failed", "The device is targeting a different chain.", "invalid-network"),
needPin: new VaultFeedback("?", "Enter the pin.", "need-pin"),
incorrectPin: new VaultFeedback("failed", "Incorrect pin code.", "incorrect-pin"),
invalidPasswordConfirmation: new VaultFeedback("failed", "Invalid password confirmation.", "invalid-password-confirm"),
wrongWallet: new VaultFeedback("failed", "This device can't sign the transaction. (Wrong device, wrong passphrase or wrong device fingerprint in your wallet settings)", "wrong-wallet"),
wrongKeyPath: new VaultFeedback("failed", "This device can't sign the transaction. (The wallet keypath in your wallet settings seems incorrect)", "wrong-keypath"),
needPassphrase: new VaultFeedback("?", "Enter the passphrase.", "need-passphrase"),
needPassphraseOnDevice: new VaultFeedback("?", "Please, enter the passphrase on the device.", "need-passphrase-on-device"),
signingTransaction: new VaultFeedback("?", "Please review and confirm the transaction on your device...", "ask-signing"),
reviewAddress: new VaultFeedback("?", "Sending... Please review the address on your device...", "ask-signing"),
signingRejected: new VaultFeedback("failed", "The user refused to sign the transaction", "user-reject"),
};
/**
@ -83,11 +81,13 @@ var vaultui = (function () {
button.show();
}
this.currentFeedback = 1;
/**
* @param {VaultFeedback} feedback
*/
function show(feedback) {
var icon = $(".vault-feedback." + feedback.category + " " + ".vault-feedback-icon");
var icon = $(".vault-feedback.vault-feedback" + self.currentFeedback + " " + ".vault-feedback-icon");
icon.removeClass();
icon.addClass("vault-feedback-icon mt-1 me-2");
if (feedback.type == "?") {
@ -100,8 +100,12 @@ var vaultui = (function () {
icon.addClass("fa fa-times-circle feedback-icon-failed");
showRetry();
}
var content = $(".vault-feedback." + feedback.category + " " + ".vault-feedback-content");
var content = $(".vault-feedback.vault-feedback" + self.currentFeedback + " " + ".vault-feedback-content");
content.html(feedback.txt);
if (feedback.type === 'ok')
self.currentFeedback++;
if (feedback.type === 'failed')
self.currentFeedback = 1;
}
function showError(json) {
if (json.hasOwnProperty("error")) {
@ -182,7 +186,7 @@ var vaultui = (function () {
if (self.retryShowing) {
await self.waitRetryPushed();
}
if (!self.bridge) {
if (!self.bridge || self.bridge.socket.readyState !== 1) {
$("#vault-dropdown").css("display", "none");
show(VaultFeedbacks.vaultLoading);
try {
@ -208,6 +212,7 @@ var vaultui = (function () {
}
return true;
};
this.askForDisplayAddress = async function (rootedKeyPath) {
if (!await self.ensureConnectedToBackend())
return false;
@ -235,6 +240,7 @@ var vaultui = (function () {
show(VaultFeedbacks.deviceFound.replace("{{0}}", json.model));
return true;
};
this.askForXPubs = async function () {
if (!await self.ensureConnectedToBackend())
return false;