mirror of
https://github.com/arx-research/libhalo.git
synced 2026-01-10 13:48:08 -05:00
Bridge/Gateway: Better exceptions when there is an error related to the transport itself, handle bridge consent properly (#328)
This commit is contained in:
committed by
GitHub
parent
7175ebb6c0
commit
2cd0957b21
@@ -10,7 +10,9 @@ const {
|
||||
NFCPermissionRequestDenied,
|
||||
NFCMethodNotSupported,
|
||||
NFCAbortedError,
|
||||
NFCOperationError
|
||||
NFCOperationError,
|
||||
NFCBadTransportError,
|
||||
NFCBridgeConsentError
|
||||
} = require("../halo/exceptions");
|
||||
const {
|
||||
parsePublicKeys, convertSignature, recoverPublicKey, sigToDer,
|
||||
@@ -37,5 +39,7 @@ module.exports = {
|
||||
NFCPermissionRequestDenied,
|
||||
NFCMethodNotSupported,
|
||||
NFCAbortedError,
|
||||
NFCOperationError
|
||||
NFCOperationError,
|
||||
NFCBadTransportError,
|
||||
NFCBridgeConsentError
|
||||
};
|
||||
|
||||
11
cli/assets/views/consent_close.html
Normal file
11
cli/assets/views/consent_close.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>HaLo Bridge Server</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.close();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -134,7 +134,11 @@ function processExecutor(ws, req, sessionId) {
|
||||
|
||||
ws.on('error', console.error);
|
||||
|
||||
ws.on('message', function message(data) {
|
||||
ws.on('close', () => {
|
||||
sobj.requestor.send(JSON.stringify({"type": "executor_disconnected"}));
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
let obj = JSON.parse(data);
|
||||
|
||||
if (obj.type === "keepalive") {
|
||||
|
||||
@@ -231,7 +231,7 @@ function wsCreateServer(args, getReaderNames) {
|
||||
let url = new URL(req.body.website);
|
||||
userConsentOrigins.add(url.protocol + '//' + url.host);
|
||||
|
||||
res.redirect(req.body.website);
|
||||
res.render('consent_close.html');
|
||||
});
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const queryString = require("query-string");
|
||||
const WebSocketAsPromised = require("websocket-as-promised");
|
||||
const {HaloLogicError, HaloTagError, NFCOperationError} = require("./exceptions");
|
||||
const {HaloLogicError, HaloTagError, NFCOperationError, NFCBadTransportError, NFCAbortedError, NFCBridgeConsentError} = require("./exceptions");
|
||||
const {haloFindBridge} = require("../web/web_utils");
|
||||
const {webDebug} = require("./util");
|
||||
|
||||
@@ -10,6 +11,7 @@ class HaloBridge {
|
||||
this.isRunning = false;
|
||||
this.lastCommand = null;
|
||||
this.lastHandle = null;
|
||||
this.url = null;
|
||||
|
||||
this.createWebSocket = options.createWebSocket
|
||||
? options.createWebSocket
|
||||
@@ -19,11 +21,17 @@ class HaloBridge {
|
||||
waitForWelcomePacket() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let welcomeWaitTimeout = setTimeout(() => {
|
||||
reject(new Error("Server doesn't send ws_connected packet for 6 seconds after accepting the connection."));
|
||||
reject(new NFCBadTransportError("Server doesn't send ws_connected packet for 6 seconds after accepting the connection."));
|
||||
}, 6000);
|
||||
|
||||
this.ws.onClose.addListener((event) => {
|
||||
reject(new Error("WebSocket closed when waiting for ws_connected packet. Reason: [" + event.code + "] " + event.reason));
|
||||
if (event.code === 4002) {
|
||||
// no user consent
|
||||
reject(new NFCBridgeConsentError());
|
||||
} else {
|
||||
reject(new NFCBadTransportError("WebSocket closed when waiting for ws_connected packet. " +
|
||||
"Reason: [" + event.code + "] " + event.reason));
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.onUnpackedMessage.addListener(data => {
|
||||
@@ -36,9 +44,9 @@ class HaloBridge {
|
||||
}
|
||||
|
||||
async connect() {
|
||||
let url = await haloFindBridge({createWebSocket: this.createWebSocket});
|
||||
this.url = await haloFindBridge({createWebSocket: this.createWebSocket});
|
||||
|
||||
this.ws = new WebSocketAsPromised(url, {
|
||||
this.ws = new WebSocketAsPromised(this.url, {
|
||||
createWebSocket: url => this.createWebSocket(url),
|
||||
packMessage: data => JSON.stringify(data),
|
||||
unpackMessage: data => JSON.parse(data),
|
||||
@@ -64,9 +72,26 @@ class HaloBridge {
|
||||
};
|
||||
}
|
||||
|
||||
getConsentURL(websiteURL, options) {
|
||||
if (!this.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.url
|
||||
.replace('ws://', 'http://')
|
||||
.replace('wss://', 'https://')
|
||||
.replace('/ws', '/consent?' + queryString.stringify({'website': websiteURL, ...options}));
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.ws && this.ws.isOpened) {
|
||||
await this.ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
async waitForHandle() {
|
||||
if (!this.ws.isOpened) {
|
||||
throw new Error("Bridge is not open.");
|
||||
throw new NFCBadTransportError("Bridge is not open.");
|
||||
}
|
||||
|
||||
if (this.lastHandle) {
|
||||
@@ -89,7 +114,7 @@ class HaloBridge {
|
||||
this.ws.onUnpackedMessage.removeListener(msgListener);
|
||||
this.ws.onClose.removeListener(closeListener);
|
||||
|
||||
reject(new Error("Bridge disconnected."));
|
||||
reject(new NFCBadTransportError("Bridge server has disconnected."));
|
||||
};
|
||||
|
||||
this.ws.onUnpackedMessage.addListener(msgListener);
|
||||
@@ -102,7 +127,7 @@ class HaloBridge {
|
||||
|
||||
if (this.isRunning) {
|
||||
webDebug('[halo-bridge] rejecting a call, there is already a call pending');
|
||||
throw new Error("Can not make multiple calls to execHaloCmd() in parallel.");
|
||||
throw new NFCAbortedError("Can not make multiple calls to execHaloCmd() in parallel.");
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
@@ -112,11 +137,18 @@ class HaloBridge {
|
||||
let handle = await this.waitForHandle();
|
||||
|
||||
webDebug('[halo-bridge] sending request to execute command', handle);
|
||||
let res = await this.ws.sendRequest({
|
||||
"type": "exec_halo",
|
||||
"handle": handle,
|
||||
"command": command
|
||||
});
|
||||
let res;
|
||||
|
||||
try {
|
||||
res = await this.ws.sendRequest({
|
||||
"type": "exec_halo",
|
||||
"handle": handle,
|
||||
"command": command
|
||||
});
|
||||
} catch (e) {
|
||||
webDebug('[halo-bridge] exception when trying to sendRequest', e);
|
||||
throw new NFCBadTransportError('Failed to send request: ' + e.toString());
|
||||
}
|
||||
|
||||
if (res.event === "exec_success") {
|
||||
webDebug('[halo-bridge] returning with success', res);
|
||||
|
||||
@@ -71,11 +71,34 @@ class NFCOperationError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The currently used transport (HaLo Bridge/Gateway) has failed permanently (for instance due to disconnect),
|
||||
* and can no longer be used without creating a completely new instance first.
|
||||
*/
|
||||
class NFCBadTransportError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "NFCBadTransportError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The current origin is not on the HaLo Bridge's allow list.
|
||||
*/
|
||||
class NFCBridgeConsentError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "NFCBridgeConsentError";
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
HaloTagError,
|
||||
HaloLogicError,
|
||||
NFCPermissionRequestDenied,
|
||||
NFCMethodNotSupported,
|
||||
NFCAbortedError,
|
||||
NFCOperationError
|
||||
NFCOperationError,
|
||||
NFCBadTransportError,
|
||||
NFCBridgeConsentError
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ const QRCode = require("qrcode");
|
||||
const WebSocketAsPromised = require("websocket-as-promised");
|
||||
const crypto = require("crypto");
|
||||
const {JWEUtil} = require("../jwe_util");
|
||||
const {HaloLogicError, HaloTagError, NFCOperationError} = require("../exceptions");
|
||||
const {HaloLogicError, HaloTagError, NFCBadTransportError, NFCAbortedError, NFCOperationError} = require("../exceptions");
|
||||
const {webDebug} = require("../util");
|
||||
|
||||
function makeQR(url) {
|
||||
@@ -21,6 +21,8 @@ class HaloGateway {
|
||||
constructor(gatewayServer, options) {
|
||||
this.jweUtil = new JWEUtil();
|
||||
this.isRunning = false;
|
||||
this.hasExecutor = false;
|
||||
this.closeTimeout = null;
|
||||
|
||||
this.lastCommand = null;
|
||||
this.gatewayServer = gatewayServer;
|
||||
@@ -63,9 +65,28 @@ class HaloGateway {
|
||||
});
|
||||
|
||||
this.ws.onUnpackedMessage.addListener(data => {
|
||||
if (data.type === "executor_connected" && this.lastCommand) {
|
||||
// existing executor connection was replaced, repeat last command
|
||||
this.ws.sendPacked(this.lastCommand);
|
||||
if (data.type === "executor_connected") {
|
||||
if (this.lastCommand) {
|
||||
// existing executor connection was replaced, repeat last command
|
||||
this.ws.sendPacked(this.lastCommand);
|
||||
}
|
||||
|
||||
this.hasExecutor = true;
|
||||
|
||||
if (this.closeTimeout !== null) {
|
||||
clearTimeout(this.closeTimeout);
|
||||
this.closeTimeout = null;
|
||||
}
|
||||
|
||||
webDebug('[halo-requestor] executor had connected');
|
||||
} else if (data.type === "executor_disconnected") {
|
||||
this.hasExecutor = false;
|
||||
|
||||
if (this.closeTimeout === null) {
|
||||
this.closeTimeout = setTimeout(() => this.ws.close(), 3000);
|
||||
}
|
||||
|
||||
webDebug('[halo-requestor] executor had disconnected');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -73,11 +94,11 @@ class HaloGateway {
|
||||
waitForWelcomePacket() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let welcomeWaitTimeout = setTimeout(() => {
|
||||
reject(new Error("Server doesn't send welcome packet for 6 seconds after accepting the connection."));
|
||||
reject(new NFCBadTransportError("Server doesn't send welcome packet for 6 seconds after accepting the connection."));
|
||||
}, 6000);
|
||||
|
||||
this.ws.onClose.addListener((event) => {
|
||||
reject(new Error("WebSocket closed when waiting for welcome packet. Reason: [" + event.code + "] " + event.reason));
|
||||
reject(new NFCBadTransportError("WebSocket closed when waiting for welcome packet. Reason: [" + event.code + "] " + event.reason));
|
||||
});
|
||||
|
||||
this.ws.onUnpackedMessage.addListener(data => {
|
||||
@@ -93,8 +114,9 @@ class HaloGateway {
|
||||
let sharedKey = await this.jweUtil.generateKey();
|
||||
|
||||
let waitPromise = this.waitForWelcomePacket();
|
||||
await this.ws.open();
|
||||
let welcomeMsg = await waitPromise;
|
||||
const promiseRes = await Promise.all([this.ws.open(), waitPromise]);
|
||||
const welcomeMsg = promiseRes[1];
|
||||
|
||||
let serverVersion = welcomeMsg.serverVersion;
|
||||
|
||||
/**
|
||||
@@ -126,7 +148,7 @@ class HaloGateway {
|
||||
waitConnected() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ws.onClose.addListener((event) => {
|
||||
reject(new Error("WebSocket closed when waiting for executor to connect. Reason: [" + event.code + "] " + event.reason));
|
||||
reject(new NFCBadTransportError("WebSocket closed when waiting for executor to connect. Reason: [" + event.code + "] " + event.reason));
|
||||
});
|
||||
|
||||
this.ws.onUnpackedMessage.addListener(data => {
|
||||
@@ -142,7 +164,17 @@ class HaloGateway {
|
||||
|
||||
if (this.isRunning) {
|
||||
webDebug('[halo-requestor] rejecting a call, there is already a call pending');
|
||||
throw new Error("Can not make multiple calls to execHaloCmd() in parallel.");
|
||||
throw new NFCAbortedError("Can not make multiple calls to execHaloCmd() in parallel.");
|
||||
}
|
||||
|
||||
if (!this.ws.isOpened) {
|
||||
webDebug('[halo-requestor] rejecting a call, socket is not open');
|
||||
throw new NFCBadTransportError("Unable to execute command, there is no connection open.");
|
||||
}
|
||||
|
||||
if (!this.hasExecutor) {
|
||||
webDebug('[halo-requestor] rejecting a call, there is no executor connected');
|
||||
throw new NFCBadTransportError("Unable to execute command, there is no executor connected.");
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
@@ -150,17 +182,24 @@ class HaloGateway {
|
||||
|
||||
try {
|
||||
webDebug('[halo-requestor] sending request to execute command', nonce, command);
|
||||
let res = await this.ws.sendRequest({
|
||||
"type": "request_cmd",
|
||||
"payload": await this.jweUtil.encrypt({
|
||||
nonce,
|
||||
command
|
||||
})
|
||||
});
|
||||
let res;
|
||||
|
||||
try {
|
||||
res = await this.ws.sendRequest({
|
||||
"type": "request_cmd",
|
||||
"payload": await this.jweUtil.encrypt({
|
||||
nonce,
|
||||
command
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
webDebug('[halo-requestor] exception when trying to sendRequest', e);
|
||||
throw new NFCBadTransportError('Failed to send request: ' + e.toString());
|
||||
}
|
||||
|
||||
if (res.type !== "result_cmd") {
|
||||
webDebug('[halo-requestor] unexpected packet type received', res);
|
||||
throw new Error("Unexpected packet type.");
|
||||
throw new NFCBadTransportError("Unexpected packet type.");
|
||||
}
|
||||
|
||||
this.lastCommand = null;
|
||||
@@ -170,12 +209,12 @@ class HaloGateway {
|
||||
out = await this.jweUtil.decrypt(res.payload);
|
||||
} catch (e) {
|
||||
webDebug('[halo-requestor] failed to validate or decrypt response JWE', e);
|
||||
throw new Error("Failed to validate or decrypt response packet.");
|
||||
throw new NFCBadTransportError("Failed to validate or decrypt response packet.");
|
||||
}
|
||||
|
||||
if (out.nonce !== nonce) {
|
||||
webDebug('[halo-requestor] mismatched nonce in reply JWE');
|
||||
throw new Error("Mismatched nonce in reply.");
|
||||
throw new NFCBadTransportError("Mismatched nonce in reply.");
|
||||
}
|
||||
|
||||
let resolution = out.response;
|
||||
@@ -209,12 +248,18 @@ class HaloGateway {
|
||||
throw e;
|
||||
} else {
|
||||
webDebug('[halo-requestor] unexpected status received');
|
||||
throw new Error("Unexpected status received.");
|
||||
throw new NFCBadTransportError("Unexpected status received.");
|
||||
}
|
||||
} finally {
|
||||
this.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.ws && this.ws.isOpened) {
|
||||
await this.ws.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -188,7 +188,7 @@ function randomBuffer() {
|
||||
}
|
||||
|
||||
function isWebDebugEnabled() {
|
||||
return window && window.localStorage && window.localStorage.getItem("DEBUG_LIBHALO_WEB") === "1";
|
||||
return typeof window !== "undefined" && window.localStorage && window.localStorage.getItem("DEBUG_LIBHALO_WEB") === "1";
|
||||
}
|
||||
|
||||
function webDebug(...args) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const WebSocketAsPromised = require('websocket-as-promised');
|
||||
const {NFCBadTransportError} = require("../halo/exceptions");
|
||||
|
||||
function haloCreateWs(url) {
|
||||
return new WebSocketAsPromised(url, {
|
||||
@@ -27,7 +28,7 @@ function runHealthCheck(url, openTimeout, createWebSocket) {
|
||||
|
||||
resolve(url);
|
||||
} else {
|
||||
reject(new Error("Unexpected WebSocket close code: " + event.code));
|
||||
reject(new NFCBadTransportError("Unexpected WebSocket close code: " + event.code));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -35,7 +36,7 @@ function runHealthCheck(url, openTimeout, createWebSocket) {
|
||||
.then(() => {
|
||||
closeTimeout = setTimeout(() => {
|
||||
wsp.close();
|
||||
reject(new Error('WebSocket didn\'t close as expected.'));
|
||||
reject(new NFCBadTransportError('WebSocket didn\'t close as expected.'));
|
||||
}, 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -102,7 +103,7 @@ async function haloFindBridge(options) {
|
||||
try {
|
||||
return await Promise.any(createChecks(wsPort, wssPort, createWebSocket));
|
||||
} catch (e) {
|
||||
throw new Error("Unable to locate halo bridge.");
|
||||
throw new NFCBadTransportError("Unable to locate halo bridge.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user