Bridge/Gateway: Better exceptions when there is an error related to the transport itself, handle bridge consent properly (#328)

This commit is contained in:
Michał Leszczyński
2024-07-04 12:56:25 -07:00
committed by GitHub
parent 7175ebb6c0
commit 2cd0957b21
9 changed files with 163 additions and 43 deletions

View File

@@ -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
};

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>HaLo Bridge Server</title>
</head>
<body>
<script>
window.close();
</script>
</body>
</html>

View File

@@ -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") {

View File

@@ -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) => {

View File

@@ -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);

View File

@@ -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
};

View File

@@ -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 = {

View File

@@ -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) {

View File

@@ -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.");
}
}
}