Add bridge server functionality (#114)

This commit is contained in:
Michał Leszczyński
2023-03-17 02:52:28 +01:00
committed by GitHub
parent 92bc7d922f
commit d00633f9d4
11 changed files with 2347 additions and 14 deletions

View File

@@ -27,6 +27,25 @@ if (process.env.__UNSAFE_ENABLE_TESTS === "1") {
});
}
let serverParser = subparsers.add_parser("server", {help: "Run local WebSocket server."});
serverParser.add_argument("-l", "--listen-host", {
help: "IP where the server should bind",
default: "127.0.0.1",
dest: "listenHost"
});
serverParser.add_argument("-p", "--listen-port", {
help: "Port where the server should bind",
type: "int",
default: 49437,
dest: "listenPort"
});
serverParser.add_argument("-a", "--allow-origins", {
help: "List of origins that are allowed to connect (semicolon-separated)",
type: "str",
default: null,
dest: "allowOrigins"
});
subparsers.add_parser("read_ndef", {help: "Read dynamic URL on the tag."});
let signParser = subparsers.add_parser("sign", {help: "Sign message using ECDSA/Keccak algorithm."});

View File

@@ -11,6 +11,7 @@ const {parseArgs} = require('./args.js');
const {execHaloCmdPCSC} = require('../index.js');
const {__runTestSuite} = require("../halo/tests");
const util = require("util");
const {wsEventCardDisconnected, wsCreateServer, wsEventCardConnected} = require("./ws_server");
const nfc = new NFC();
let stopPCSCTimeout = null;
@@ -38,13 +39,18 @@ nfc.on('reader', reader => {
reader.autoProcessing = false;
reader.on('card', card => {
clearTimeout(stopPCSCTimeout);
stopPCSCTimeout = setTimeout(stopPCSC, 4000, "timeout", args.output);
if (args.name === "server") {
wsEventCardConnected(reader);
return;
}
if (args.name === "pcsc_detect") {
console.log("Tag inserted:", reader.reader.name, '(Type: ' + card.type + ', ATR: ' + card.atr.toString('hex').toUpperCase() + ')');
}
clearTimeout(stopPCSCTimeout);
stopPCSCTimeout = setTimeout(stopPCSC, 4000, "timeout", args.output);
(async () => {
let res = await checkCard(reader);
@@ -88,6 +94,14 @@ nfc.on('reader', reader => {
})();
});
reader.on('card.off', card => {
wsEventCardDisconnected(reader);
});
reader.on('end', () => {
wsEventCardDisconnected(reader);
});
reader.on('error', err => {
console.log(`${reader.reader.name} an error occurred`, err);
});
@@ -125,4 +139,8 @@ function stopPCSC(code, output) {
}
}
stopPCSCTimeout = setTimeout(stopPCSC, 4000, "timeout", args.output);
if (args.name === "server") {
wsCreateServer(args);
} else {
stopPCSCTimeout = setTimeout(stopPCSC, 4000, "timeout", args.output);
}

23
cli/package-lock.json generated
View File

@@ -9,7 +9,8 @@
"version": "1.1.8",
"license": "MIT",
"dependencies": {
"nfc-pcsc": "^0.8.1"
"nfc-pcsc": "^0.8.1",
"ws": "^8.13.0"
},
"bin": {
"halocli": "cli.js"
@@ -1652,6 +1653,26 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@@ -4,26 +4,33 @@
"description": "HaLo Command Line Interface Tool for PC/SC",
"contributors": [
{
"name" : "Michal Leszczynski",
"email" : "ml@arx.sh",
"url" : "https://github.com/icedevml"
"name": "Michal Leszczynski",
"email": "ml@arx.sh",
"url": "https://github.com/icedevml"
},
{
"name" : "Cameron Robertson",
"email" : "cameron@arx.sh",
"url" : "https://github.com/ccamrobertson"
"name": "Cameron Robertson",
"email": "cameron@arx.sh",
"url": "https://github.com/ccamrobertson"
}
],
"keywords": ["blockchain", "ethereum", "bitcoin", "nfc"],
"keywords": [
"blockchain",
"ethereum",
"bitcoin",
"nfc"
],
"license": "MIT",
"homepage": "https://github.com/arx-research/libhalo#readme",
"bugs": {
"url" : "https://github.com/arx-research/libhalo/issues/new/choose"
"url": "https://github.com/arx-research/libhalo/issues/new/choose"
},
"main": "cli.js",
"bin": "cli.js",
"pkg": {
"targets": ["node16"],
"targets": [
"node16"
],
"outputPath": "dist",
"assets": [
"node_modules/@pokusew/pcsclite/build/Release/pcsclite.node"
@@ -34,7 +41,8 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"nfc-pcsc": "^0.8.1"
"nfc-pcsc": "^0.8.1",
"ws": "^8.13.0"
},
"devDependencies": {
"pkg": "^5.8.0"

145
cli/ws_server.js Normal file
View File

@@ -0,0 +1,145 @@
const { WebSocketServer } = require('ws');
const crypto = require('crypto').webcrypto;
const {execHaloCmdPCSC} = require('../index.js');
let wss = null;
let currentWsClient = null;
let currentState = null;
function generateHandle() {
return Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64');
}
function generateAuthPin() {
let val = Buffer.from(crypto.getRandomValues(new Uint8Array(3))).toString('hex');
return val.slice(0, 3) + '-' + val.slice(3);
}
function sendToCurrentWs(ws, data) {
if (currentWsClient !== null && (ws === null || currentWsClient === ws)) {
console.log('send', data);
currentWsClient.send(JSON.stringify(data));
return true;
}
return false;
}
function wsEventCardConnected(reader) {
if (currentState) {
sendToCurrentWs(null, {
"event": "handle_removed",
"uid": null,
"data": {
"handle": currentState.handle
}
});
}
let handle = generateHandle();
currentState = {"handle": handle, "reader": reader};
sendToCurrentWs(null, {
"event": "handle_added",
"uid": null,
"data": {
"handle": handle
}
});
}
function wsEventCardDisconnected(reader) {
if (currentState !== null && currentState.reader === reader) {
sendToCurrentWs(null, {
"event": "handle_removed",
"uid": null,
"data": {
"handle": currentState.handle
}
});
currentState = null;
}
}
function wsCreateServer(args) {
wss = new WebSocketServer({host: args.listenHost, port: args.listenPort});
wss.on('connection', (ws, req) => {
let originHostname = new URL(req.headers.origin).hostname;
if (args.allowOrigins) {
let allowedOrigins = args.allowOrigins.split(';');
if (!allowedOrigins.includes(req.headers.origin)) {
ws.close(4002, "Connecting origin is not on the configured allow list.");
return;
}
} else if (originHostname !== "localhost" || originHostname !== "127.0.0.1") {
ws.close(4003, "Connecting origin is not localhost. No other allowed origins are configured.");
return;
}
if (currentWsClient) {
currentWsClient.close(4001, "New client has connected. Server has dropped the current connection.");
}
currentWsClient = ws;
ws.on('error', console.error);
ws.on('message', async function message(data) {
if (currentWsClient !== ws) {
return;
}
let packet = JSON.parse(data);
console.log('recv', packet);
if (packet.type === "exec_halo") {
try {
if (!currentState || packet.handle !== currentState.handle) {
throw new Error("Invalid handle.");
}
let res = await execHaloCmdPCSC(packet.command, currentState.reader);
sendToCurrentWs(ws, {
"event": "exec_success",
"uid": packet.uid,
"data": {
"res": res
}
});
} catch (e) {
sendToCurrentWs(ws, {
"event": "exec_exception",
"uid": packet.uid,
"data": {
"exception": {
"message": String(e),
"stack": e.stack
}
}
});
}
}
});
sendToCurrentWs(ws, {
"event": "ws_connected",
"uid": null,
"data": {}
});
if (currentState) {
sendToCurrentWs(null, {
"event": "handle_added",
"uid": null,
"data": {
"handle": currentState.handle
}
});
}
});
}
module.exports = {wsCreateServer, wsEventCardConnected, wsEventCardDisconnected};

2
web/examples/ws/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/node_modules/
/dist/

2009
web/examples/ws/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
{
"name": "libhalo-wsdemo",
"version": "1.0.0",
"description": "",
"main": "wsdemo.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"devDependencies": {
"webpack": "^5.76.2",
"webpack-cli": "^5.0.1"
},
"dependencies": {
"websocket-as-promised": "^2.0.1"
}
}

View File

@@ -0,0 +1,10 @@
module.exports = {
entry: {
app: './wsdemo.js',
},
output: {
filename: 'wsdemo.js'
},
mode: 'production',
target: 'web',
};

View File

@@ -0,0 +1,65 @@
<script src="dist/wsdemo.js"></script>
<script>
function log(data) {
console.log(data);
document.getElementById('log').innerText += '\n' + data;
}
let wsp = createWs('ws://localhost:49437');
async function processTag(handle) {
let res = await wsp.sendRequest({
"type": "exec_halo",
"handle": handle,
"command": {
"name": "get_pkeys"
}
});
if (res.event === "exec_exception") {
log("!!! ERROR !!! Failed to execute HaLo command.");
}
log(JSON.stringify(res));
res = await wsp.sendRequest({
"type": "exec_halo",
"handle": handle,
"command": {
"name": "sign",
"message": "010203",
"keyNo": 1
}
});
if (res.event === "exec_exception") {
log("!!! ERROR !!! Failed to execute HaLo command.");
}
log(JSON.stringify(res));
}
wsp.onUnpackedMessage.addListener(async ev => {
if (ev.event !== "exec_success" && ev.event !== "exec_exception") {
log(JSON.stringify(ev));
}
if (ev.event === "handle_added") {
await processTag(ev.data.handle);
}
});
wsp.onClose.addListener(event => {
if (event.code === 4001) {
log('Connection closed, new client has connected.');
} else {
log('Connection closed: ' + event.code);
}
});
wsp.open();
</script>
<pre id="log">Connecting to the server...</pre>

18
web/examples/ws/wsdemo.js Normal file
View File

@@ -0,0 +1,18 @@
const WebSocketAsPromised = require('websocket-as-promised');
function createWs(url) {
return new WebSocketAsPromised(url, {
packMessage: data => JSON.stringify(data),
unpackMessage: data => JSON.parse(data),
attachRequestId: (data, requestId) => Object.assign({uid: requestId}, data),
extractRequestId: data => data && data.uid
});
}
module.exports = {createWs};
if (window) {
Object.keys(module.exports).forEach((key) => {
window[key] = module.exports[key];
});
}