mirror of
https://github.com/EvernodeXRPL/hpcore.git
synced 2026-04-29 15:37:59 +00:00
User protocol upgrade and js client lib. (#191)
* Unified js client lib for browser and nodejs. * Client lib multiple connections support. * Implemented server challenge response. * Contract guid and version validation. * Server key validation. * User json message encoding improvements.
This commit is contained in:
@@ -1,294 +0,0 @@
|
||||
window.HotPocket = (() => {
|
||||
|
||||
const protocols = {
|
||||
json: "json"
|
||||
}
|
||||
Object.freeze(protocols);
|
||||
|
||||
const events = {
|
||||
disconnect: "disconnect",
|
||||
contractOutput: "contractOutput",
|
||||
contractReadResponse: "contractReadResponse"
|
||||
}
|
||||
Object.freeze(events);
|
||||
|
||||
const fromHexString = hexString =>
|
||||
new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
|
||||
|
||||
const toHexString = bytes =>
|
||||
bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
|
||||
|
||||
const KeyGenerator = {
|
||||
generate: function (privateKeyHex = null) {
|
||||
|
||||
if (!privateKeyHex) {
|
||||
const keys = sodium.crypto_sign_keypair();
|
||||
return {
|
||||
privateKey: keys.privateKey,
|
||||
publicKey: keys.publicKey
|
||||
}
|
||||
}
|
||||
else {
|
||||
const binPrivateKey = fromHexString(privateKeyHex);
|
||||
return {
|
||||
privateKey: Uint8Array.from(binPrivateKey),
|
||||
publicKey: Uint8Array.from(binPrivateKey.slice(32))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
function EventEmitter() {
|
||||
const registrations = {};
|
||||
|
||||
this.on = (eventName, listener) => {
|
||||
if (!registrations[eventName])
|
||||
registrations[eventName] = [];
|
||||
registrations[eventName].push(listener);
|
||||
}
|
||||
|
||||
this.emit = (eventName, value) => {
|
||||
if (registrations[eventName])
|
||||
registrations[eventName].forEach(listener => listener(value));
|
||||
}
|
||||
}
|
||||
|
||||
HotPocketClient = function HotPocketClient(contractId, server, keys) {
|
||||
|
||||
let ws = null;
|
||||
const protocol = protocols.json; // We only support json in browser.
|
||||
const msgHelper = new MessageHelper(keys, protocol);
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
let handshakeResolver = null;
|
||||
let statResponseResolvers = [];
|
||||
let contractInputResolvers = {};
|
||||
|
||||
this.connect = function () {
|
||||
return new Promise(resolve => {
|
||||
|
||||
handshakeResolver = resolve;
|
||||
|
||||
ws = new WebSocket(server);
|
||||
|
||||
ws.addEventListener("close", () => {
|
||||
// If there are any ongoing resolvers resolve them with error output.
|
||||
|
||||
handshakeResolver && handshakeResolver(false);
|
||||
handshakeResolver = null;
|
||||
|
||||
statResponseResolvers.forEach(resolver => resolver(null));
|
||||
statResponseResolvers = [];
|
||||
|
||||
Object.values(contractInputResolvers).forEach(resolver => resolver(null));
|
||||
contractInputResolvers = {};
|
||||
|
||||
emitter.emit(events.disconnect);
|
||||
});
|
||||
|
||||
ws.onmessage = async (rcvd) => {
|
||||
|
||||
msg = await rcvd.data.text();
|
||||
|
||||
try {
|
||||
m = msgHelper.deserializeMessage(msg);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log("Exception deserializing: ");
|
||||
console.log(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m.type == 'handshake_challenge') {
|
||||
// Check whether contract id is matching if specified.
|
||||
if (contractId && m.contract_id != contractId) {
|
||||
console.error("Contract id mismatch.")
|
||||
ws.close();
|
||||
}
|
||||
|
||||
// sign the challenge and send back the response
|
||||
const response = msgHelper.createHandshakeResponse(m.challenge);
|
||||
ws.send(JSON.stringify(response));
|
||||
|
||||
setTimeout(() => {
|
||||
// If we are still connected, report handshaking as successful.
|
||||
// (If websocket disconnects, handshakeResolver will be null)
|
||||
handshakeResolver && handshakeResolver(true);
|
||||
handshakeResolver = null;
|
||||
}, 100);
|
||||
}
|
||||
else if (m.type == 'contract_read_response') {
|
||||
emitter.emit(events.contractReadResponse, msgHelper.deserializeOutput(m.content));
|
||||
}
|
||||
else if (m.type == 'contract_input_status') {
|
||||
const sigKey = (typeof m.input_sig === "string") ? m.input_sig : m.input_sig.toString("hex");
|
||||
const resolver = contractInputResolvers[sigKey];
|
||||
if (resolver) {
|
||||
if (m.status == "accepted")
|
||||
resolver("ok");
|
||||
else
|
||||
resolver(m.reason);
|
||||
delete contractInputResolvers[sigKey];
|
||||
}
|
||||
}
|
||||
else if (m.type == 'contract_output') {
|
||||
emitter.emit(events.contractOutput, msgHelper.deserializeOutput(m.content));
|
||||
}
|
||||
else if (m.type == "stat_response") {
|
||||
statResponseResolvers.forEach(resolver => {
|
||||
resolver({
|
||||
lcl: m.lcl,
|
||||
lclSeqNo: m.lcl_seqno
|
||||
});
|
||||
})
|
||||
statResponseResolvers = [];
|
||||
}
|
||||
else {
|
||||
console.log("Received unrecognized message: type:" + m.type);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.on = function (event, listener) {
|
||||
emitter.on(event, listener);
|
||||
}
|
||||
|
||||
this.close = function () {
|
||||
return new Promise(resolve => {
|
||||
try {
|
||||
ws.addEventListener("close", resolve);
|
||||
ws.close();
|
||||
} catch (error) {
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.getStatus = function () {
|
||||
const p = new Promise(resolve => {
|
||||
statResponseResolvers.push(resolve);
|
||||
});
|
||||
|
||||
// If this is the only awaiting stat request, then send an actual stat request.
|
||||
// Otherwise simply wait for the previously sent request.
|
||||
if (statResponseResolvers.length == 1) {
|
||||
const msg = msgHelper.createStatusRequest();
|
||||
ws.send(msgHelper.serializeObject(msg));
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
this.sendContractInput = async function (input, nonce = null, maxLclOffset = null) {
|
||||
|
||||
if (!maxLclOffset)
|
||||
maxLclOffset = 10;
|
||||
|
||||
if (!nonce)
|
||||
nonce = (new Date()).getTime().toString();
|
||||
else
|
||||
nonce = nonce.toString();
|
||||
|
||||
// Acquire the current lcl and add the specified offset.
|
||||
const stat = await this.getStatus();
|
||||
if (!stat)
|
||||
return new Promise(resolve => resolve("ledger_status_error"));
|
||||
const maxLclSeqNo = stat.lclSeqNo + maxLclOffset;
|
||||
|
||||
const msg = msgHelper.createContractInput(input, nonce, maxLclSeqNo);
|
||||
const sigKey = (typeof msg.sig === "string") ? msg.sig : msg.sig.toString("hex");
|
||||
const p = new Promise(resolve => {
|
||||
contractInputResolvers[sigKey] = resolve;
|
||||
});
|
||||
|
||||
ws.send(msgHelper.serializeObject(msg));
|
||||
return p;
|
||||
}
|
||||
|
||||
this.sendContractReadRequest = function (request) {
|
||||
const msg = msgHelper.createReadRequest(request);
|
||||
ws.send(msgHelper.serializeObject(msg));
|
||||
}
|
||||
}
|
||||
|
||||
function MessageHelper(keys, protocol) {
|
||||
|
||||
this.binaryEncode = function (data) {
|
||||
return toHexString(data);
|
||||
}
|
||||
|
||||
this.serializeObject = function (obj) {
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
|
||||
this.deserializeMessage = function (m) {
|
||||
return JSON.parse(m);
|
||||
}
|
||||
|
||||
this.serializeInput = function (input) {
|
||||
return (typeof input === 'string' || input instanceof String) ? input : input.toString();
|
||||
}
|
||||
|
||||
this.deserializeOutput = function (content) {
|
||||
return content;
|
||||
}
|
||||
|
||||
this.createHandshakeResponse = function (challenge) {
|
||||
// For handshake response encoding Hot Pocket always uses json.
|
||||
// Handshake response will specify the protocol to use for subsequent messages.
|
||||
const sigBytes = sodium.crypto_sign_detached(challenge, keys.privateKey);
|
||||
|
||||
return {
|
||||
type: "handshake_response",
|
||||
challenge: challenge,
|
||||
sig: toHexString(sigBytes),
|
||||
pubkey: "ed" + toHexString(keys.publicKey),
|
||||
protocol: protocol
|
||||
}
|
||||
}
|
||||
|
||||
this.createContractInput = function (input, nonce, maxLclSeqNo) {
|
||||
|
||||
if (input.length == 0)
|
||||
return null;
|
||||
|
||||
const inpContainer = {
|
||||
input: this.serializeInput(input),
|
||||
nonce: nonce,
|
||||
max_lcl_seqno: maxLclSeqNo
|
||||
}
|
||||
|
||||
const serlializedInpContainer = this.serializeObject(inpContainer);
|
||||
const sigBytes = sodium.crypto_sign_detached(serlializedInpContainer, keys.privateKey);
|
||||
|
||||
const signedInpContainer = {
|
||||
type: "contract_input",
|
||||
input_container: serlializedInpContainer,
|
||||
sig: this.binaryEncode(sigBytes)
|
||||
}
|
||||
|
||||
return signedInpContainer;
|
||||
}
|
||||
|
||||
this.createReadRequest = function (request) {
|
||||
|
||||
if (request.length == 0)
|
||||
return null;
|
||||
|
||||
return {
|
||||
type: "contract_read_request",
|
||||
content: this.serializeInput(request)
|
||||
}
|
||||
}
|
||||
|
||||
this.createStatusRequest = function () {
|
||||
return { type: 'stat' };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
KeyGenerator: KeyGenerator,
|
||||
Client: HotPocketClient,
|
||||
events: events,
|
||||
}
|
||||
})();
|
||||
@@ -1,16 +0,0 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>HotPocket test page</title>
|
||||
<script src="hp-browser-client-lib.js"></script>
|
||||
<script src="test.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/libsodium-wrappers/0.5.4/sodium.min.js"
|
||||
integrity="sha512-oRfU7aik4u7f0dPAKgOyA4+bb/YRGfAaD5RA4Z3Mb2ycPcGDs+k8qAnDNd7ouruoqlIHSuGVaTTlEs91Gvd37A=="
|
||||
crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
HotPocket browser client test page.
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,33 +0,0 @@
|
||||
window.sodium = {
|
||||
onload: async function () {
|
||||
const keys = HotPocket.KeyGenerator.generate(); // Can provide existing hex private key as parameter as well.
|
||||
const hpc = new HotPocket.Client(null, "wss://localhost:8081", keys);
|
||||
|
||||
if (!await hpc.connect()) {
|
||||
console.log('Connection failed.');
|
||||
return;
|
||||
}
|
||||
console.log('HotPocket Connected.');
|
||||
|
||||
// This will get fired if HP server disconnects unexpectedly.
|
||||
hpc.on(HotPocket.events.disconnect, () => {
|
||||
console.log('Server disconnected');
|
||||
})
|
||||
|
||||
// This will get fired when contract sends an output.
|
||||
hpc.on(HotPocket.events.contractOutput, (output) => {
|
||||
console.log("Contract output>> " + output);
|
||||
})
|
||||
|
||||
// This will get fired when contract sends a read response.
|
||||
hpc.on(HotPocket.events.contractReadResponse, (response) => {
|
||||
console.log("Contract read response>> " + response);
|
||||
})
|
||||
|
||||
hpc.sendContractReadRequest("Hello");
|
||||
hpc.sendContractInput("World!")
|
||||
|
||||
// When we need to close HP connection:
|
||||
// hpc.close();
|
||||
}
|
||||
};
|
||||
79
examples/js_client/browser-test.html
Normal file
79
examples/js_client/browser-test.html
Normal file
@@ -0,0 +1,79 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>HotPocket test page</title>
|
||||
<script src="hp-client-lib.js"></script>
|
||||
|
||||
<!--Hot Pocket client library requires libsodium js for cryptographic operations.-->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/libsodium-wrappers/0.5.4/sodium.min.js"
|
||||
integrity="sha512-oRfU7aik4u7f0dPAKgOyA4+bb/YRGfAaD5RA4Z3Mb2ycPcGDs+k8qAnDNd7ouruoqlIHSuGVaTTlEs91Gvd37A=="
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
async function testFunc() {
|
||||
const keys = await HotPocket.generateKeys(); // Can provide existing hex private key as parameter as well.
|
||||
|
||||
// Simple connection to single server without any validations.
|
||||
const hpc = await HotPocket.createClient(["wss://localhost:8081"], keys);
|
||||
|
||||
// Maintain multiple connections with contract id/version and server key validation.
|
||||
// const options = {
|
||||
// contractId: "3c349abe-4d70-4f50-9fa6-018f1f2530ab",
|
||||
// contractVersion: "1.0",
|
||||
// validServerKeys: [
|
||||
// "ed5597c207bbd251997b7133d5d83a2c6ab9600810edf0bdb43f4004852b8c9e17",
|
||||
// "ed0b2ffd75b67c3979d3c362d8350ec190f053fa27d3dfcb2eced426efd1d3affc",
|
||||
// "edd2e1a817387d68adf8adb1d0b339e3f04868c3c81bf6a7472647f10657e31aa1"
|
||||
// ],
|
||||
// protocol: HotPocket.protocols.json,
|
||||
// requiredConnectionCount: 2,
|
||||
// connectionTimeoutMs: 5000
|
||||
// };
|
||||
// const hpc = await HotPocket.createClient(
|
||||
// [
|
||||
// "wss://localhost:8081",
|
||||
// "wss://localhost:8082",
|
||||
// "wss://localhost:8083"
|
||||
// ], keys, options);
|
||||
|
||||
if (!await hpc.connect()) {
|
||||
console.log('Connection failed.');
|
||||
return;
|
||||
}
|
||||
console.log('HotPocket Connected.');
|
||||
|
||||
// This will get fired if HP server disconnects unexpectedly.
|
||||
hpc.on(HotPocket.events.disconnect, () => {
|
||||
console.log('Disconnected');
|
||||
})
|
||||
|
||||
// This will get fired as servers connects/disconnects after the initial connection establishment.
|
||||
hpc.on(HotPocket.events.connectionChange, (server, action) => {
|
||||
console.log(server + " " + action);
|
||||
})
|
||||
|
||||
// This will get fired when contract sends an output.
|
||||
hpc.on(HotPocket.events.contractOutput, (output) => {
|
||||
console.log("Contract output>> " + output);
|
||||
})
|
||||
|
||||
// This will get fired when contract sends a read response.
|
||||
hpc.on(HotPocket.events.contractReadResponse, (response) => {
|
||||
console.log("Contract read response>> " + response);
|
||||
})
|
||||
|
||||
hpc.sendContractReadRequest("Hello");
|
||||
hpc.sendContractInput("World!")
|
||||
|
||||
// When we need to close HP connection:
|
||||
// await hpc.close();
|
||||
}
|
||||
testFunc();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
HotPocket browser client test page.
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,13 +1,12 @@
|
||||
const fs = require('fs');
|
||||
const readline = require('readline');
|
||||
const { exit } = require('process');
|
||||
const bson = require('bson');
|
||||
var path = require("path");
|
||||
const HotPocket = require('./hp-node-client-lib');
|
||||
const HotPocket = require('./hp-client-lib');
|
||||
|
||||
async function main() {
|
||||
|
||||
const keys = await HotPocket.KeyGenerator.generate();
|
||||
const keys = await HotPocket.generateKeys();
|
||||
|
||||
const pkhex = Buffer.from(keys.publicKey).toString('hex');
|
||||
console.log('My public key is: ' + pkhex);
|
||||
@@ -15,19 +14,32 @@ async function main() {
|
||||
let server = 'wss://localhost:8080'
|
||||
if (process.argv.length == 3) server = 'wss://localhost:' + process.argv[2]
|
||||
if (process.argv.length == 4) server = 'wss://' + process.argv[2] + ':' + process.argv[3]
|
||||
const hpc = new HotPocket.Client(null, server, keys, HotPocket.protocols.bson);
|
||||
const hpc = await HotPocket.createClient([server], keys, { protocol: HotPocket.protocols.bson });
|
||||
|
||||
// Establish HotPocket connection.
|
||||
if (!await hpc.connect()) {
|
||||
console.log('Connection failed.');
|
||||
exit();
|
||||
return;
|
||||
}
|
||||
console.log('HotPocket Connected.');
|
||||
|
||||
// start listening for stdin
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
// On ctrl + c we should close HP connection gracefully.
|
||||
rl.on('SIGINT', () => {
|
||||
console.log('SIGINT received...');
|
||||
rl.close();
|
||||
hpc.close();
|
||||
});
|
||||
|
||||
// This will get fired if HP server disconnects unexpectedly.
|
||||
hpc.on(HotPocket.events.disconnect, () => {
|
||||
console.log('Server diconnected');
|
||||
exit();
|
||||
console.log('Disconnected');
|
||||
rl.close();
|
||||
})
|
||||
|
||||
// This will get fired when contract sends an output.
|
||||
@@ -66,18 +78,7 @@ async function main() {
|
||||
console.log("Unknown read request result.");
|
||||
}
|
||||
})
|
||||
|
||||
// On ctrl + c we should close HP connection gracefully.
|
||||
process.once('SIGINT', function () {
|
||||
console.log('SIGINT received...');
|
||||
hpc.close();
|
||||
});
|
||||
|
||||
// start listening for stdin
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
console.log("Ready to accept inputs.");
|
||||
|
||||
const input_pump = () => {
|
||||
729
examples/js_client/hp-client-lib.js
Normal file
729
examples/js_client/hp-client-lib.js
Normal file
@@ -0,0 +1,729 @@
|
||||
(() => {
|
||||
|
||||
// Whether we are in Browser or NodeJs.
|
||||
const isBrowser = !(typeof window === 'undefined');
|
||||
|
||||
// In browser, avoid duplicate initializations.
|
||||
if (isBrowser && window.HotPocket)
|
||||
return;
|
||||
|
||||
const supportedHpVersion = "0.0";
|
||||
const serverChallengeSize = 16;
|
||||
const connectionCheckIntervalMs = 1000;
|
||||
const recentActivityThresholdMs = 3000;
|
||||
|
||||
// External dependency references.
|
||||
let WebSocket = null;
|
||||
let sodium = null;
|
||||
let bson = null;
|
||||
|
||||
/*--- Included in public interface. ---*/
|
||||
const protocols = {
|
||||
json: "json",
|
||||
bson: "bson" // (Requires nodejs or browserified hp client library on Browser)
|
||||
}
|
||||
Object.freeze(protocols);
|
||||
|
||||
/*--- Included in public interface. ---*/
|
||||
const events = {
|
||||
disconnect: "disconnect",
|
||||
contractOutput: "contractOutput",
|
||||
contractReadResponse: "contractReadResponse",
|
||||
connectionChange: "connectionChange"
|
||||
}
|
||||
Object.freeze(events);
|
||||
|
||||
/*--- Included in public interface. ---*/
|
||||
const generateKeys = async (privateKeyHex = null) => {
|
||||
|
||||
await initSodium();
|
||||
|
||||
if (!privateKeyHex) {
|
||||
const keys = sodium.crypto_sign_keypair();
|
||||
return {
|
||||
privateKey: keys.privateKey,
|
||||
publicKey: keys.publicKey
|
||||
}
|
||||
}
|
||||
else {
|
||||
const binPrivateKey = hexToUint8Array(privateKeyHex);
|
||||
return {
|
||||
privateKey: Uint8Array.from(binPrivateKey),
|
||||
publicKey: Uint8Array.from(binPrivateKey.slice(32))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*--- Included in public interface. ---*/
|
||||
const createClient = async (servers, clientKeys, options) => {
|
||||
|
||||
const defaultOptions = {
|
||||
contractId: null,
|
||||
contractVersion: null,
|
||||
validServerKeys: null,
|
||||
protocol: protocols.json,
|
||||
requiredConnectionCount: 1,
|
||||
connectionTimeoutMs: 5000
|
||||
};
|
||||
const opt = options ? { ...defaultOptions, ...options } : defaultOptions;
|
||||
|
||||
if (!clientKeys)
|
||||
throw "clientKeys not specified.";
|
||||
if (opt.contractId == "")
|
||||
throw "contractId not specified. Specify null to bypass contract id validation.";
|
||||
if (opt.contractVersion == "")
|
||||
throw "contractVersion not specified. Specify null to bypass contract version validation.";
|
||||
if (!opt.protocol || (opt.protocol != protocols.json && opt.protocol != protocols.bson))
|
||||
throw "Valid protocol not specified.";
|
||||
if (!opt.requiredConnectionCount || opt.requiredConnectionCount == 0)
|
||||
throw "requiredConnectionCount must be greater than 0.";
|
||||
if (!opt.connectionTimeoutMs || opt.connectionTimeoutMs == 0)
|
||||
throw "Connection timeout must be greater than 0.";
|
||||
|
||||
await initSodium();
|
||||
initWebSocket();
|
||||
if (opt.protocol == protocols.bson)
|
||||
initBson();
|
||||
|
||||
// Load servers and serverKeys to object keys to avoid duplicates.
|
||||
|
||||
const serversLookup = {};
|
||||
servers && servers.forEach(s => {
|
||||
const url = s.trim();
|
||||
if (url.length > 0)
|
||||
serversLookup[url] = true
|
||||
});
|
||||
if (Object.keys(serversLookup).length == 0)
|
||||
throw "servers not specified.";
|
||||
if (opt.requiredConnectionCount > Object.keys(serversLookup).length)
|
||||
throw "requiredConnectionCount is higher than no. of servers.";
|
||||
|
||||
let serverKeysLookup = null;
|
||||
if (opt.validServerKeys) {
|
||||
serverKeysLookup = {};
|
||||
opt.validServerKeys.forEach(k => {
|
||||
const key = k.trim();
|
||||
if (key.length > 0)
|
||||
serverKeysLookup[key] = true
|
||||
});
|
||||
}
|
||||
|
||||
if (serverKeysLookup && Object.keys(serverKeysLookup).length == 0)
|
||||
throw "serverKeys must contain at least one key. Specify null to bypass key validation.";
|
||||
|
||||
return new HotPocketClient(opt.contractId, opt.contractVersion, clientKeys, serversLookup, serverKeysLookup, opt.protocol, opt.requiredConnectionCount, opt.connectionTimeoutMs);
|
||||
}
|
||||
|
||||
function HotPocketClient(contractId, contractVersion, clientKeys, serversLookup, serverKeysLookup, protocol, requiredConnectionCount, connectionTimeoutMs) {
|
||||
|
||||
let emitter = new EventEmitter();
|
||||
|
||||
const nodes = Object.keys(serversLookup).map(s => {
|
||||
return {
|
||||
server: s, // Server address.
|
||||
connection: null, // Hot Pocket connection (if any).
|
||||
lastActivity: 0 // Last connection activity timestamp.
|
||||
}
|
||||
});
|
||||
|
||||
let status = 0; //0:none, 1:connected, 2:closed
|
||||
|
||||
// This will get fired whenever the required connection count gets fullfilled.
|
||||
let initialConnectSuccess = null;
|
||||
|
||||
// Tracks when was the earliest time that we were missing some required connections.
|
||||
// 0 indicates we are no missing any connections.
|
||||
let connectionsMissingFrom = new Date().getTime();
|
||||
|
||||
// Checks for missing connections and attempts to establish them.
|
||||
const reviewConnections = () => {
|
||||
|
||||
if (status == 2)
|
||||
return;
|
||||
|
||||
// Check for connection changes periodically.
|
||||
setTimeout(() => {
|
||||
reviewConnections();
|
||||
}, connectionCheckIntervalMs);
|
||||
|
||||
// Check whether we have fullfilled all required connections.
|
||||
if (nodes.filter(n => n.connection && n.connection.isConnected()).length == requiredConnectionCount) {
|
||||
connectionsMissingFrom = 0;
|
||||
initialConnectSuccess && initialConnectSuccess(true);
|
||||
initialConnectSuccess = null;
|
||||
status = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionsMissingFrom == 0) {
|
||||
// Reaching here means we moved from connections-fullfilled state to missing-connections state.
|
||||
connectionsMissingFrom = new Date().getTime();
|
||||
}
|
||||
else if ((new Date().getTime() - connectionsMissingFrom) > connectionTimeoutMs) {
|
||||
|
||||
// This means we were not able to maintain required connection count for the entire timeout period.
|
||||
|
||||
console.log("Missing-connections timeout reached.");
|
||||
|
||||
// Close and cleanup all connections if we hit the timeout.
|
||||
this.close().then(() => {
|
||||
if (initialConnectSuccess) {
|
||||
initialConnectSuccess(false);
|
||||
initialConnectSuccess = null;
|
||||
}
|
||||
else {
|
||||
emitter && emitter.emit(events.disconnect);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Reaching here means we should attempt to establish more connections if we have available slots.
|
||||
let currentConnectionCount = nodes.filter(n => n.connection).length;
|
||||
if (currentConnectionCount == requiredConnectionCount)
|
||||
return;
|
||||
|
||||
// Find out available slots.
|
||||
// Skip nodes that are already connected or is currently establishing connection.
|
||||
// Skip nodes that have recently shown some connection activity.
|
||||
// Give priority to nodes that have not shown any activity recently.
|
||||
const freeNodes = nodes.filter(n => !n.connection && (new Date().getTime() - n.lastActivity) > recentActivityThresholdMs);
|
||||
freeNodes.sort((a, b) => a.lastActivity - b.lastActivity); // Oldest activity comes first.
|
||||
|
||||
while (currentConnectionCount < requiredConnectionCount && freeNodes.length > 0) {
|
||||
|
||||
// Get the next available node.
|
||||
const n = freeNodes.shift();
|
||||
n.connection = new HotPocketConnection(contractId, contractVersion, clientKeys, n.server, serverKeysLookup, protocol, connectionTimeoutMs, emitter);
|
||||
n.lastActivity = new Date().getTime();
|
||||
|
||||
n.connection.connect().then(success => {
|
||||
if (!success)
|
||||
n.connection = null;
|
||||
else
|
||||
emitter && emitter.emit(events.connectionChange, n.server, "add");
|
||||
});
|
||||
|
||||
n.connection.onClose = () => {
|
||||
n.connection = null;
|
||||
emitter && emitter.emit(events.connectionChange, n.server, "remove");
|
||||
};
|
||||
|
||||
currentConnectionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
this.connect = () => {
|
||||
|
||||
if (status > 0)
|
||||
return;
|
||||
|
||||
reviewConnections();
|
||||
return new Promise(resolve => {
|
||||
initialConnectSuccess = resolve;
|
||||
})
|
||||
}
|
||||
|
||||
this.close = async () => {
|
||||
|
||||
if (status == 2)
|
||||
return;
|
||||
|
||||
status = 2;
|
||||
emitter.clear(events.connectionChange);
|
||||
emitter.clear(events.contractOutput);
|
||||
emitter.clear(events.contractReadResponse);
|
||||
|
||||
// Close all nodes connections.
|
||||
await Promise.all(nodes.filter(n => n.connection).map(n => n.connection.close()));
|
||||
nodes.forEach(n => n.connection = null);
|
||||
}
|
||||
|
||||
this.on = (event, listener) => {
|
||||
emitter.on(event, listener);
|
||||
}
|
||||
|
||||
this.sendContractInput = async (input, nonce = null, maxLclOffset = null) => {
|
||||
if (status == 2)
|
||||
return;
|
||||
|
||||
await Promise.all(
|
||||
nodes.filter(n => n.connection && n.connection.isConnected())
|
||||
.map(n => n.connection.sendContractInput(input, nonce, maxLclOffset)));
|
||||
}
|
||||
|
||||
this.sendContractReadRequest = (request) => {
|
||||
if (status == 2)
|
||||
return;
|
||||
|
||||
nodes.filter(n => n.connection && n.connection.isConnected()).forEach(n => {
|
||||
n.connection.sendContractReadRequest(request);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function HotPocketConnection(contractId, contractVersion, clientKeys, server, serverKeysLookup, protocol, connectionTimeoutMs, emitter) {
|
||||
|
||||
// Create message helper with JSON protocol initially.
|
||||
// After challenge handshake, we will change this to use the protocol specified by user.
|
||||
const msgHelper = new MessageHelper(clientKeys, protocols.json);
|
||||
|
||||
let connectionStatus = 0; // 0:none, 1:server challenge sent, 2:handshake complete.
|
||||
let serverChallenge = null; // The hex challenge we have issued to the server.
|
||||
let reportedContractId = null;
|
||||
let reportedContractVersion = null;
|
||||
|
||||
let ws = null;
|
||||
let handshakeTimer = null; // Timer to track connection handshake timeout.
|
||||
let handshakeResolver = null;
|
||||
let closeResolver = null;
|
||||
let statResponseResolvers = [];
|
||||
let contractInputResolvers = {};
|
||||
|
||||
const handshakeMessageHandler = (m) => {
|
||||
|
||||
if (connectionStatus == 0 && m.type == "user_challenge" && m.hp_version && m.contract_id) {
|
||||
|
||||
if (m.hp_version != supportedHpVersion) {
|
||||
console.log(`Incompatible Hot Pocket server version. Expected:${supportedHpVersion} Got:${m.hp_version}`);
|
||||
return false;
|
||||
}
|
||||
else if (!m.contract_id) {
|
||||
console.log("Server did not specify contract id.");
|
||||
return false;
|
||||
}
|
||||
else if (contractId && m.contract_id != contractId) {
|
||||
console.log(`Contract id mismatch. Expected:${contractId} Got:${m.contract_id}`);
|
||||
return false;
|
||||
}
|
||||
else if (!m.contract_version) {
|
||||
console.log("Server did not specify contract version.");
|
||||
return false;
|
||||
}
|
||||
else if (contractVersion && m.contract_version != contractVersion) {
|
||||
console.log(`Contract version mismatch. Expected:${contractVersion} Got:${m.contract_version}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
reportedContractId = m.contract_id;
|
||||
reportedContractVersion = m.contract_version;
|
||||
|
||||
// Generate the challenge we are sending to server.
|
||||
serverChallenge = uint8ArrayToHex(sodium.randombytes_buf(serverChallengeSize));
|
||||
|
||||
// Sign the challenge and send back the response
|
||||
const response = msgHelper.createUserChallengeResponse(m.challenge, serverChallenge, protocol);
|
||||
ws.send(msgHelper.serializeObject(response));
|
||||
|
||||
connectionStatus = 1;
|
||||
return true;
|
||||
}
|
||||
else if (connectionStatus == 1 && serverChallenge && m.type == "server_challenge_response" && m.sig && m.pubkey) {
|
||||
|
||||
// If server keys has been specified, validate whether this server's pubkey is among the valid list.
|
||||
if (serverKeysLookup && !serverKeysLookup[m.pubkey]) {
|
||||
console.log(`${server} key '${m.pubkey}' not among the valid keys.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify server challenge response.
|
||||
const stringToVerify = serverChallenge + reportedContractId + reportedContractVersion;
|
||||
const serverPubkeyHex = m.pubkey.substring(2); // Skip 'ed' prefix;
|
||||
if (!sodium.crypto_sign_verify_detached(hexToUint8Array(m.sig), stringToVerify, hexToUint8Array(serverPubkeyHex))) {
|
||||
console.log(`${server} challenge response verification failed.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
clearTimeout(handshakeTimer); // Cancel the handshake timeout monitor.
|
||||
handshakeTimer = null;
|
||||
serverChallenge = null; // Clear the sent challenge as we no longer need it.
|
||||
msgHelper.useProtocol(protocol); // Here onwards, use the message protocol specified by user.
|
||||
connectionStatus = 2; // Handshake complete.
|
||||
|
||||
// If we are still connected, report handshaking as successful.
|
||||
// (If websocket disconnects, handshakeResolver will be already null)
|
||||
handshakeResolver && handshakeResolver(true);
|
||||
console.log(`Connected to ${server}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`${server} invalid message during handshake. Connection status:${connectionStatus}`);
|
||||
console.log(m);
|
||||
return false;
|
||||
}
|
||||
|
||||
const contractMessageHandler = (m) => {
|
||||
|
||||
if (m.type == "contract_read_response") {
|
||||
emitter && emitter.emit(events.contractReadResponse, msgHelper.deserializeOutput(m.content));
|
||||
}
|
||||
else if (m.type == "contract_input_status") {
|
||||
const sigKey = msgHelper.stringifySignature(m.input_sig);
|
||||
const resolver = contractInputResolvers[sigKey];
|
||||
if (resolver) {
|
||||
if (m.status == "accepted")
|
||||
resolver("ok");
|
||||
else
|
||||
resolver(m.reason);
|
||||
delete contractInputResolvers[sigKey];
|
||||
}
|
||||
}
|
||||
else if (m.type == "contract_output") {
|
||||
emitter && emitter.emit(events.contractOutput, msgHelper.deserializeOutput(m.content));
|
||||
}
|
||||
else if (m.type == "stat_response") {
|
||||
statResponseResolvers.forEach(resolver => {
|
||||
resolver({
|
||||
lcl: m.lcl,
|
||||
lclSeqNo: m.lcl_seqno
|
||||
});
|
||||
})
|
||||
statResponseResolvers = [];
|
||||
}
|
||||
else {
|
||||
console.log("Received unrecognized contract message: type:" + m.type);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const messageHandler = async (rcvd) => {
|
||||
|
||||
const data = (connectionStatus < 2 || protocol == protocols.json) ?
|
||||
(isBrowser ? await rcvd.data.text() : rcvd.data) :
|
||||
(isBrowser ? await rcvd.data.arrayBuffer() : rcvd.data);
|
||||
|
||||
try {
|
||||
m = msgHelper.deserializeMessage(data);
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
console.log("Exception deserializing: ");
|
||||
console.log(data || rcvd);
|
||||
|
||||
// If we get invalid message during handshake, close the socket.
|
||||
if (connectionStatus < 2)
|
||||
this.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let isValid = false;
|
||||
if (connectionStatus < 2)
|
||||
isValid = handshakeMessageHandler(m);
|
||||
else if (connectionStatus == 2)
|
||||
isValid = contractMessageHandler(m);
|
||||
|
||||
if (!isValid) {
|
||||
// If we get invalid message during handshake, close the socket.
|
||||
if (connectionStatus < 2)
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
const openHandler = () => {
|
||||
ws.addEventListener("message", messageHandler);
|
||||
ws.addEventListener("close", closeHandler);
|
||||
|
||||
handshakeTimer = setTimeout(() => {
|
||||
// If handshake does not complete within timeout, close the connection.
|
||||
this.close();
|
||||
handshakeTimer = null;
|
||||
}, connectionTimeoutMs);
|
||||
}
|
||||
|
||||
const closeHandler = () => {
|
||||
|
||||
if (closeResolver)
|
||||
console.log("Closing connection to " + server);
|
||||
else
|
||||
console.log("Disconnected from " + server);
|
||||
|
||||
emitter = null;
|
||||
|
||||
if (handshakeTimer)
|
||||
clearTimeout(handshakeTimer);
|
||||
|
||||
// If there are any ongoing resolvers resolve them with error output.
|
||||
|
||||
handshakeResolver && handshakeResolver(false);
|
||||
handshakeResolver = null;
|
||||
|
||||
statResponseResolvers.forEach(resolver => resolver(null));
|
||||
statResponseResolvers = [];
|
||||
|
||||
Object.values(contractInputResolvers).forEach(resolver => resolver(null));
|
||||
contractInputResolvers = {};
|
||||
|
||||
this.onClose && this.onClose();
|
||||
closeResolver && closeResolver();
|
||||
}
|
||||
|
||||
const errorHandler = (e) => {
|
||||
handshakeResolver && handshakeResolver(false);
|
||||
}
|
||||
|
||||
this.isConnected = () => {
|
||||
return connectionStatus == 2;
|
||||
};
|
||||
|
||||
this.connect = () => {
|
||||
console.log("Connecting to " + server);
|
||||
return new Promise(resolve => {
|
||||
|
||||
ws = isBrowser ? new WebSocket(server) : new WebSocket(server, { rejectUnauthorized: false });
|
||||
handshakeResolver = resolve;
|
||||
ws.addEventListener("error", errorHandler);
|
||||
ws.addEventListener("open", openHandler);
|
||||
});
|
||||
}
|
||||
|
||||
this.close = () => {
|
||||
if (ws.readyState == WebSocket.OPEN) {
|
||||
return new Promise(resolve => {
|
||||
closeResolver = resolve;
|
||||
ws.close();
|
||||
});
|
||||
}
|
||||
else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
this.getStatus = () => {
|
||||
|
||||
if (connectionStatus != 2)
|
||||
return Promise.resolve(null);
|
||||
|
||||
const p = new Promise(resolve => {
|
||||
statResponseResolvers.push(resolve);
|
||||
});
|
||||
|
||||
// If this is the only awaiting stat request, then send an actual stat request.
|
||||
// Otherwise simply wait for the previously sent request.
|
||||
if (statResponseResolvers.length == 1) {
|
||||
const msg = msgHelper.createStatusRequest();
|
||||
ws.send(msgHelper.serializeObject(msg));
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
this.sendContractInput = async (input, nonce = null, maxLclOffset = null) => {
|
||||
|
||||
if (connectionStatus != 2)
|
||||
return null;
|
||||
|
||||
if (!maxLclOffset)
|
||||
maxLclOffset = 10;
|
||||
|
||||
if (!nonce)
|
||||
nonce = (new Date()).getTime().toString();
|
||||
else
|
||||
nonce = nonce.toString();
|
||||
|
||||
// Acquire the current lcl and add the specified offset.
|
||||
const stat = await this.getStatus();
|
||||
if (!stat)
|
||||
return new Promise(resolve => resolve("ledger_status_error"));
|
||||
const maxLclSeqNo = stat.lclSeqNo + maxLclOffset;
|
||||
|
||||
const msg = msgHelper.createContractInput(input, nonce, maxLclSeqNo);
|
||||
const sigKey = msgHelper.stringifySignature(msg.sig);
|
||||
const p = new Promise(resolve => {
|
||||
contractInputResolvers[sigKey] = resolve;
|
||||
});
|
||||
|
||||
ws.send(msgHelper.serializeObject(msg));
|
||||
return p;
|
||||
}
|
||||
|
||||
this.sendContractReadRequest = (request) => {
|
||||
|
||||
if (connectionStatus != 2)
|
||||
return;
|
||||
|
||||
const msg = msgHelper.createReadRequest(request);
|
||||
ws.send(msgHelper.serializeObject(msg));
|
||||
}
|
||||
}
|
||||
|
||||
function MessageHelper(keys, protocol) {
|
||||
|
||||
this.useProtocol = (p) => {
|
||||
protocol = p;
|
||||
}
|
||||
|
||||
this.binaryEncode = (data) => {
|
||||
return protocol == protocols.json ?
|
||||
uint8ArrayToHex(data) :
|
||||
(Buffer.isBuffer(data) ? data : Buffer.from(data));
|
||||
}
|
||||
|
||||
this.serializeObject = (obj) => {
|
||||
return protocol == protocols.json ? JSON.stringify(obj) : bson.serialize(obj);
|
||||
}
|
||||
|
||||
this.deserializeMessage = (m) => {
|
||||
return protocol == protocols.json ? JSON.parse(m) : bson.deserialize(m);
|
||||
}
|
||||
|
||||
this.serializeInput = (input) => {
|
||||
return protocol == protocols.json ?
|
||||
((typeof input === "string" || input instanceof String) ? input : input.toString()) :
|
||||
(Buffer.isBuffer(input) ? input : Buffer.from(input));
|
||||
}
|
||||
|
||||
this.deserializeOutput = (content) => {
|
||||
return protocol == protocols.json ? content : content.buffer;
|
||||
}
|
||||
|
||||
// Used for generating strings to hold signature as js object keys.
|
||||
this.stringifySignature = (sig) => {
|
||||
if (typeof sig === 'string' || sig instanceof String)
|
||||
return sig;
|
||||
else if (sig instanceof Uint8Array)
|
||||
return uint8ArrayToHex(sig);
|
||||
else if (sig.buffer) // BSON binary.
|
||||
return uint8ArrayToHex(new Uint8Array(sig.buffer));
|
||||
else
|
||||
throw "Cannot stringify signature.";
|
||||
}
|
||||
|
||||
this.createUserChallengeResponse = (userChallenge, serverChallenge, msgProtocol) => {
|
||||
// For challenge response encoding Hot Pocket always uses json.
|
||||
// Challenge response will specify the protocol to use for contract messages.
|
||||
const sigBytes = sodium.crypto_sign_detached(userChallenge, keys.privateKey);
|
||||
|
||||
return {
|
||||
type: "user_challenge_response",
|
||||
sig: this.binaryEncode(sigBytes),
|
||||
pubkey: "ed" + this.binaryEncode(keys.publicKey),
|
||||
server_challenge: serverChallenge,
|
||||
protocol: msgProtocol
|
||||
}
|
||||
}
|
||||
|
||||
this.createContractInput = (input, nonce, maxLclSeqNo) => {
|
||||
|
||||
if (input.length == 0)
|
||||
return null;
|
||||
|
||||
const inpContainer = {
|
||||
input: this.serializeInput(input),
|
||||
nonce: nonce,
|
||||
max_lcl_seqno: maxLclSeqNo
|
||||
}
|
||||
|
||||
const serlializedInpContainer = this.serializeObject(inpContainer);
|
||||
const sigBytes = sodium.crypto_sign_detached(serlializedInpContainer, keys.privateKey);
|
||||
|
||||
const signedInpContainer = {
|
||||
type: "contract_input",
|
||||
input_container: serlializedInpContainer,
|
||||
sig: this.binaryEncode(sigBytes)
|
||||
}
|
||||
|
||||
return signedInpContainer;
|
||||
}
|
||||
|
||||
this.createReadRequest = (request) => {
|
||||
if (request.length == 0)
|
||||
return null;
|
||||
|
||||
return {
|
||||
type: "contract_read_request",
|
||||
content: this.serializeInput(request)
|
||||
}
|
||||
}
|
||||
|
||||
this.createStatusRequest = () => {
|
||||
return { type: "stat" };
|
||||
}
|
||||
}
|
||||
|
||||
function hexToUint8Array(hexString) {
|
||||
return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
|
||||
}
|
||||
|
||||
function uint8ArrayToHex(bytes) {
|
||||
return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
|
||||
}
|
||||
|
||||
function EventEmitter() {
|
||||
const registrations = {};
|
||||
|
||||
this.on = (eventName, listener) => {
|
||||
if (!registrations[eventName])
|
||||
registrations[eventName] = [];
|
||||
registrations[eventName].push(listener);
|
||||
}
|
||||
|
||||
this.emit = (eventName, ...value) => {
|
||||
if (registrations[eventName])
|
||||
registrations[eventName].forEach(listener => listener(...value));
|
||||
}
|
||||
|
||||
this.clear = (eventName) => {
|
||||
if (eventName)
|
||||
delete registrations[eventName]
|
||||
else
|
||||
Object.keys(registrations).forEach(k => delete registrations[k]);
|
||||
}
|
||||
}
|
||||
|
||||
// Set sodium reference.
|
||||
async function initSodium() {
|
||||
if (sodium) // If already set, do nothing.
|
||||
return;
|
||||
else if (isBrowser && window.sodium) { // If no parameter specified, try to get from window.sodium.
|
||||
sodium = window.sodium;
|
||||
}
|
||||
else if (isBrowser && !window.sodium) { // If sodium not yet loaded in browser, wait for sodium ready.
|
||||
await new Promise(resolve => {
|
||||
window.sodium = {
|
||||
onload: async function (sodiumRef) {
|
||||
sodium = sodiumRef;
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
else if (!isBrowser) { // nodejs
|
||||
sodium = require('libsodium-wrappers');
|
||||
await sodium.ready;
|
||||
}
|
||||
}
|
||||
|
||||
// Set bson reference.
|
||||
function initBson() {
|
||||
if (bson) // If already set, do nothing.
|
||||
return;
|
||||
else if (isBrowser && window.BSON) // If no parameter specified, try to get from window.BSON.
|
||||
bson = window.BSON;
|
||||
else if (!isBrowser) // nodejs
|
||||
bson = require('bson');
|
||||
}
|
||||
|
||||
// Set WebSocket reference.
|
||||
function initWebSocket() {
|
||||
if (WebSocket) // If already set, do nothing.
|
||||
return;
|
||||
else if (isBrowser && window.WebSocket) // If no parameter specified, try to get from window.WebSocket.
|
||||
WebSocket = window.WebSocket;
|
||||
else if (!isBrowser) // nodejs
|
||||
WebSocket = require('ws');
|
||||
}
|
||||
|
||||
const hotPocketLib = {
|
||||
generateKeys,
|
||||
createClient,
|
||||
events,
|
||||
protocols
|
||||
}
|
||||
|
||||
if (isBrowser)
|
||||
window.HotPocket = hotPocketLib;
|
||||
else
|
||||
module.exports = hotPocketLib;
|
||||
})();
|
||||
@@ -160,9 +160,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
||||
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"bn.js": {
|
||||
"version": "5.1.3",
|
||||
@@ -430,12 +430,12 @@
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
|
||||
"integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"requires": {
|
||||
"base64-js": "^1.0.2",
|
||||
"ieee754": "^1.1.4"
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"buffer-from": {
|
||||
@@ -454,6 +454,7 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.1.tgz",
|
||||
"integrity": "sha512-xowrxvpxojqkagPcWRQVXZl0YXhRhAtBEIq3VoER1NH5Mw1n1o0ojdspp+GS2J//2gCVyrzQDApQ4unGF+QOoA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"node-gyp-build": "~3.7.0"
|
||||
}
|
||||
@@ -1330,9 +1331,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
|
||||
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
@@ -1378,10 +1379,13 @@
|
||||
}
|
||||
},
|
||||
"is-arguments": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
|
||||
"integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==",
|
||||
"dev": true
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz",
|
||||
"integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"is-buffer": {
|
||||
"version": "1.1.6",
|
||||
@@ -1434,11 +1438,6 @@
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
||||
"dev": true
|
||||
},
|
||||
"isomorphic-ws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
|
||||
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="
|
||||
},
|
||||
"json-stable-stringify": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz",
|
||||
@@ -1662,9 +1661,9 @@
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"multi-stage-sourcemap": {
|
||||
@@ -1720,7 +1719,8 @@
|
||||
"node-gyp-build": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.7.0.tgz",
|
||||
"integrity": "sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w=="
|
||||
"integrity": "sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==",
|
||||
"dev": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -2491,6 +2491,7 @@
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.2.tgz",
|
||||
"integrity": "sha512-SwV++i2gTD5qh2XqaPzBnNX88N6HdyhQrNNRykvcS0QKvItV9u3vPEJr+X5Hhfb1JC0r0e1alL0iB09rY8+nmw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"node-gyp-build": "~3.7.0"
|
||||
}
|
||||
17
examples/js_client/package.json
Normal file
17
examples/js_client/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"scripts": {
|
||||
"build-node": "browserify --node -p tinyify hp-client-lib.js -o dist/hp-node-client-lib.js",
|
||||
"build-browser": "browserify -p tinyify hp-client-lib.js -o dist/hp-browser-client-lib.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"libsodium-wrappers": "0.7.6",
|
||||
"ws": "7.1.2",
|
||||
"bson": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tinyify": "3.0.0",
|
||||
"browserify": "16.5.2",
|
||||
"utf-8-validate": "5.0.2",
|
||||
"bufferutil": "4.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
const readline = require('readline');
|
||||
const { exit } = require('process');
|
||||
const HotPocket = require('./hp-node-client-lib');
|
||||
const HotPocket = require('./hp-client-lib');
|
||||
|
||||
async function main() {
|
||||
const keys = await HotPocket.KeyGenerator.generate();
|
||||
|
||||
const keys = await HotPocket.generateKeys();
|
||||
|
||||
const pkhex = Buffer.from(keys.publicKey).toString('hex');
|
||||
console.log('My public key is: ' + pkhex);
|
||||
@@ -11,19 +11,32 @@ async function main() {
|
||||
let server = 'wss://localhost:8080'
|
||||
if (process.argv.length == 3) server = 'wss://localhost:' + process.argv[2]
|
||||
if (process.argv.length == 4) server = 'wss://' + process.argv[2] + ':' + process.argv[3]
|
||||
const hpc = new HotPocket.Client(null, server, keys, HotPocket.protocols.json);
|
||||
const hpc = await HotPocket.createClient([server], keys);
|
||||
|
||||
// Establish HotPocket connection.
|
||||
if (!await hpc.connect()) {
|
||||
console.log('Connection failed.');
|
||||
exit();
|
||||
return;
|
||||
}
|
||||
console.log('HotPocket Connected.');
|
||||
|
||||
// start listening for stdin
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
// On ctrl + c we should close HP connection gracefully.
|
||||
rl.on('SIGINT', () => {
|
||||
console.log('SIGINT received...');
|
||||
rl.close();
|
||||
hpc.close();
|
||||
});
|
||||
|
||||
// This will get fired if HP server disconnects unexpectedly.
|
||||
hpc.on(HotPocket.events.disconnect, () => {
|
||||
console.log('Server disconnected');
|
||||
exit();
|
||||
console.log('Disconnected');
|
||||
rl.close();
|
||||
})
|
||||
|
||||
// This will get fired when contract sends an output.
|
||||
@@ -36,17 +49,6 @@ async function main() {
|
||||
console.log("Contract read response>> " + response);
|
||||
})
|
||||
|
||||
// On ctrl + c we should close HP connection gracefully.
|
||||
process.once('SIGINT', function () {
|
||||
console.log('SIGINT received...');
|
||||
hpc.close();
|
||||
});
|
||||
|
||||
// start listening for stdin
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
console.log("Ready to accept inputs.");
|
||||
|
||||
const input_pump = () => {
|
||||
@@ -1 +0,0 @@
|
||||
{"publicKey":"705bf26354ee4c63c0e5d5d883c07cefc3196d049bd3825f827eb3bc23ead035","privateKey":"07ce6f7d6f0da38d5956ecc4ea1d18de77fc8bded089eb52199a46ffe2098c88705bf26354ee4c63c0e5d5d883c07cefc3196d049bd3825f827eb3bc23ead035","keyType":"ed25519"}
|
||||
@@ -1,306 +0,0 @@
|
||||
const WebSocket = require('isomorphic-ws');
|
||||
const sodium = require('libsodium-wrappers');
|
||||
const EventEmitter = require('events');
|
||||
const bson = require('bson');
|
||||
|
||||
// Whether we are in NodeJS or Browser.
|
||||
const isNodeJS = (typeof window === 'undefined');
|
||||
|
||||
const protocols = {
|
||||
json: "json",
|
||||
bson: "bson"
|
||||
}
|
||||
Object.freeze(protocols);
|
||||
|
||||
const events = {
|
||||
disconnect: "disconnect",
|
||||
contractOutput: "contractOutput",
|
||||
contractReadResponse: "contractReadResponse"
|
||||
}
|
||||
Object.freeze(events);
|
||||
|
||||
const HotPocketKeyGenerator = {
|
||||
generate: async function (privateKeyHex = null) {
|
||||
await sodium.ready;
|
||||
|
||||
if (!privateKeyHex) {
|
||||
const keys = sodium.crypto_sign_keypair();
|
||||
return {
|
||||
privateKey: keys.privateKey,
|
||||
publicKey: keys.publicKey
|
||||
}
|
||||
}
|
||||
else {
|
||||
const binPrivateKey = Buffer.from(privateKeyHex, "hex");
|
||||
return {
|
||||
privateKey: Uint8Array.from(binPrivateKey),
|
||||
publicKey: Uint8Array.from(binPrivateKey.slice(32))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
function HotPocketClient(contractId, server, keys, protocol = protocols.json) {
|
||||
|
||||
let ws = null;
|
||||
const msgHelper = new MessageHelper(keys, protocol);
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
let handshakeResolver = null;
|
||||
let statResponseResolvers = [];
|
||||
let contractInputResolvers = {};
|
||||
|
||||
this.connect = function () {
|
||||
return new Promise(resolve => {
|
||||
|
||||
handshakeResolver = resolve;
|
||||
|
||||
if (isNodeJS) {
|
||||
ws = new WebSocket(server, {
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
}
|
||||
else {
|
||||
ws = new WebSocket(server);
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
|
||||
// If there are any ongoing resolvers resolve them with error output.
|
||||
|
||||
handshakeResolver && handshakeResolver(false);
|
||||
handshakeResolver = null;
|
||||
|
||||
statResponseResolvers.forEach(resolver => resolver(null));
|
||||
statResponseResolvers = [];
|
||||
|
||||
Object.values(contractInputResolvers).forEach(resolver => resolver(null));
|
||||
contractInputResolvers = {};
|
||||
|
||||
emitter.emit(events.disconnect);
|
||||
};
|
||||
|
||||
ws.onmessage = async (rcvd) => {
|
||||
|
||||
if (isNodeJS) {
|
||||
msg = rcvd.data;
|
||||
}
|
||||
else {
|
||||
msg = (handshakeResolver || protocol == protocols.json) ?
|
||||
await rcvd.data.text() :
|
||||
Buffer.from(await rcvd.data.arrayBuffer());
|
||||
}
|
||||
|
||||
try {
|
||||
// Use JSON if we are still in handshake phase.
|
||||
m = handshakeResolver ? JSON.parse(msg) : msgHelper.deserializeMessage(msg);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log("Exception deserializing: ");
|
||||
console.log(msg)
|
||||
return;
|
||||
}
|
||||
|
||||
if (m.type == 'handshake_challenge') {
|
||||
// Check whether contract id is matching if specified.
|
||||
if (contractId && m.contract_id != contractId) {
|
||||
console.error("Contract id mismatch.")
|
||||
ws.close();
|
||||
}
|
||||
|
||||
// Sign the challenge and send back the response
|
||||
const response = msgHelper.createHandshakeResponse(m.challenge);
|
||||
ws.send(JSON.stringify(response));
|
||||
|
||||
setTimeout(() => {
|
||||
// If we are still connected, report handshaking as successful.
|
||||
// (If websocket disconnects, handshakeResolver will be null)
|
||||
handshakeResolver && handshakeResolver(true);
|
||||
handshakeResolver = null;
|
||||
}, 100);
|
||||
}
|
||||
else if (m.type == 'contract_read_response') {
|
||||
emitter.emit(events.contractReadResponse, msgHelper.deserializeOutput(m.content));
|
||||
}
|
||||
else if (m.type == 'contract_input_status') {
|
||||
const sigKey = (typeof m.input_sig === "string") ? m.input_sig : m.input_sig.toString("hex");
|
||||
const resolver = contractInputResolvers[sigKey];
|
||||
if (resolver) {
|
||||
if (m.status == "accepted")
|
||||
resolver("ok");
|
||||
else
|
||||
resolver(m.reason);
|
||||
delete contractInputResolvers[sigKey];
|
||||
}
|
||||
}
|
||||
else if (m.type == 'contract_output') {
|
||||
emitter.emit(events.contractOutput, msgHelper.deserializeOutput(m.content));
|
||||
}
|
||||
else if (m.type == "stat_response") {
|
||||
statResponseResolvers.forEach(resolver => {
|
||||
resolver({
|
||||
lcl: m.lcl,
|
||||
lclSeqNo: m.lcl_seqno
|
||||
});
|
||||
})
|
||||
statResponseResolvers = [];
|
||||
}
|
||||
else {
|
||||
console.log("Received unrecognized message: type:" + m.type);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.on = function (event, listener) {
|
||||
emitter.on(event, listener);
|
||||
}
|
||||
|
||||
this.close = function () {
|
||||
return new Promise(resolve => {
|
||||
try {
|
||||
ws.onclose = resolve;
|
||||
ws.on("close", resolve);
|
||||
ws.close();
|
||||
} catch (error) {
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.getStatus = function () {
|
||||
const p = new Promise(resolve => {
|
||||
statResponseResolvers.push(resolve);
|
||||
});
|
||||
|
||||
// If this is the only awaiting stat request, then send an actual stat request.
|
||||
// Otherwise simply wait for the previously sent request.
|
||||
if (statResponseResolvers.length == 1) {
|
||||
const msg = msgHelper.createStatusRequest();
|
||||
ws.send(msgHelper.serializeObject(msg));
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
this.sendContractInput = async function (input, nonce = null, maxLclOffset = null) {
|
||||
|
||||
if (!maxLclOffset)
|
||||
maxLclOffset = 10;
|
||||
|
||||
if (!nonce)
|
||||
nonce = (new Date()).getTime().toString();
|
||||
else
|
||||
nonce = nonce.toString();
|
||||
|
||||
// Acquire the current lcl and add the specified offset.
|
||||
const stat = await this.getStatus();
|
||||
if (!stat)
|
||||
return new Promise(resolve => resolve("ledger_status_error"));
|
||||
const maxLclSeqNo = stat.lclSeqNo + maxLclOffset;
|
||||
|
||||
const msg = msgHelper.createContractInput(input, nonce, maxLclSeqNo);
|
||||
const sigKey = (typeof msg.sig === "string") ? msg.sig : msg.sig.toString("hex");
|
||||
const p = new Promise(resolve => {
|
||||
contractInputResolvers[sigKey] = resolve;
|
||||
});
|
||||
|
||||
ws.send(msgHelper.serializeObject(msg));
|
||||
return p;
|
||||
}
|
||||
|
||||
this.sendContractReadRequest = function (request) {
|
||||
const msg = msgHelper.createReadRequest(request);
|
||||
ws.send(msgHelper.serializeObject(msg));
|
||||
}
|
||||
}
|
||||
|
||||
function MessageHelper(keys, protocol) {
|
||||
|
||||
this.binaryEncode = function (data) {
|
||||
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||
return protocol == protocols.json ? buffer.toString("hex") : buffer;
|
||||
}
|
||||
|
||||
this.serializeObject = function (obj) {
|
||||
return protocol == protocols.json ? JSON.stringify(obj) : bson.serialize(obj);
|
||||
}
|
||||
|
||||
this.deserializeMessage = function (m) {
|
||||
return protocol == protocols.json ? JSON.parse(m) : bson.deserialize(m);
|
||||
}
|
||||
|
||||
this.serializeInput = function (input) {
|
||||
return protocol == protocols.json ?
|
||||
input.toString() :
|
||||
Buffer.isBuffer(input) ? input : Buffer.from(input);
|
||||
}
|
||||
|
||||
this.deserializeOutput = function (content) {
|
||||
return (protocol == protocols.json) ? content : content.buffer;
|
||||
}
|
||||
|
||||
this.createHandshakeResponse = function (challenge) {
|
||||
// For handshake response encoding Hot Pocket always uses json.
|
||||
// Handshake response will specify the protocol to use for subsequent messages.
|
||||
const sigBytes = sodium.crypto_sign_detached(challenge, keys.privateKey);
|
||||
return {
|
||||
type: "handshake_response",
|
||||
challenge: challenge,
|
||||
sig: Buffer.from(sigBytes).toString("hex"),
|
||||
pubkey: "ed" + Buffer.from(keys.publicKey).toString("hex"),
|
||||
protocol: protocol
|
||||
}
|
||||
}
|
||||
|
||||
this.createContractInput = function (input, nonce, maxLclSeqNo) {
|
||||
|
||||
if (input.length == 0)
|
||||
return null;
|
||||
|
||||
const inpContainer = {
|
||||
input: this.serializeInput(input),
|
||||
nonce: nonce,
|
||||
max_lcl_seqno: maxLclSeqNo
|
||||
}
|
||||
|
||||
const serlializedInpContainer = this.serializeObject(inpContainer);
|
||||
const sigBytes = sodium.crypto_sign_detached(Buffer.from(serlializedInpContainer), keys.privateKey);
|
||||
|
||||
const signedInpContainer = {
|
||||
type: "contract_input",
|
||||
input_container: serlializedInpContainer,
|
||||
sig: this.binaryEncode(sigBytes)
|
||||
}
|
||||
|
||||
return signedInpContainer;
|
||||
}
|
||||
|
||||
this.createReadRequest = function (request) {
|
||||
|
||||
if (request.length == 0)
|
||||
return null;
|
||||
|
||||
return {
|
||||
type: "contract_read_request",
|
||||
content: this.serializeInput(request)
|
||||
}
|
||||
}
|
||||
|
||||
this.createStatusRequest = function () {
|
||||
return { type: 'stat' };
|
||||
}
|
||||
}
|
||||
|
||||
const exportObj = {
|
||||
KeyGenerator: HotPocketKeyGenerator,
|
||||
Client: HotPocketClient,
|
||||
events: events,
|
||||
protocols: protocols
|
||||
};
|
||||
|
||||
if (isNodeJS) {
|
||||
module.exports = exportObj;
|
||||
}
|
||||
else {
|
||||
window.HotPocket = exportObj;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
const { HotPocketClient, HotPocketKeyGenerator, HotPocketEvents } = require('./hp-node-client-lib');
|
||||
|
||||
async function main() {
|
||||
|
||||
const clientCount = 3;
|
||||
const clients = [];
|
||||
for (let i = 1; i <= clientCount; i++) {
|
||||
clients.push(new RoboClient('wss://localhost:', 8081, i.toString()));
|
||||
}
|
||||
|
||||
await Promise.all(clients.map(c => c.connect()));
|
||||
console.log("Clients connected.");
|
||||
|
||||
await Promise.all(clients.map(c => c.sendInputs(["A", "B", "C"])));
|
||||
console.log("Clients submitted.");
|
||||
|
||||
// await Promise.all(clients.map(c => c.disconnect()));
|
||||
// console.log("Clients closed.");
|
||||
}
|
||||
|
||||
function RoboClient(server, port, clientId) {
|
||||
|
||||
this.connect = async () => {
|
||||
this.keys = await HotPocketKeyGenerator.generate();
|
||||
this.hpclient = new HotPocketClient(null, server + port, this.keys);
|
||||
|
||||
|
||||
if (!await this.hpclient.connect()) {
|
||||
this.log('Connection failed.');
|
||||
}
|
||||
this.log('HotPocket Connected.');
|
||||
|
||||
// This will get fired if HP server disconnects unexpectedly.
|
||||
this.hpclient.on(HotPocketEvents.disconnect, () => {
|
||||
this.log('Server disconnected');
|
||||
})
|
||||
|
||||
// This will get fired when contract sends an output.
|
||||
this.hpclient.on(HotPocketEvents.contractOutput, (output) => {
|
||||
this.log("Contract output>> " + Buffer.from(output, "hex"));
|
||||
})
|
||||
|
||||
// This will get fired when contract sends a read response.
|
||||
this.hpclient.on(HotPocketEvents.contractReadResponse, (response) => {
|
||||
this.log("Contract read response>> " + Buffer.from(response, "hex"));
|
||||
})
|
||||
}
|
||||
|
||||
this.disconnect = async () => {
|
||||
await this.hpclient.close();
|
||||
}
|
||||
|
||||
this.sendInputs = async (inputs) => {
|
||||
|
||||
let idx = 1;
|
||||
let tasks = [];
|
||||
inputs.forEach(inp => {
|
||||
const nonce = clientId.toString() + '-' + idx.toString();
|
||||
tasks.push(this.hpclient.sendContractInput((clientId + inp), nonce).then(submissionStatus => {
|
||||
if (submissionStatus && submissionStatus != "ok")
|
||||
this.log("Input submission failed. reason: " + submissionStatus);
|
||||
}));
|
||||
idx++;
|
||||
})
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
|
||||
this.log = (text) => {
|
||||
console.log(clientId + ": " + text)
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"scripts": {
|
||||
"buildnode": "browserify --node -p tinyify hp-node-client-lib.js -o dist/hp-node-client-lib.js",
|
||||
"buildbrowser": "browserify -p tinyify hp-node-client-lib.js -o dist/hp-browsercompat-client-lib.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"libsodium-wrappers": "0.7.6",
|
||||
"ws": "7.1.2",
|
||||
"isomorphic-ws": "4.0.1",
|
||||
"bson": "4.0.4",
|
||||
"utf-8-validate": "5.0.2",
|
||||
"bufferutil": "4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "16.5.2",
|
||||
"tinyify": "3.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
const { HotPocketContract } = require("./hp-contract-lib");
|
||||
const HotPocket = require("./hp-contract-lib");
|
||||
const fs = require('fs');
|
||||
|
||||
// HP smart contract is defined as a function which takes HP ExecutionContext as an argument.
|
||||
@@ -24,10 +24,7 @@ const echoContract = async (ctx) => {
|
||||
const msg = buf.toString();
|
||||
|
||||
const output = (msg == "ts") ? fs.readFileSync("exects.txt").toString() : ("Echoing: " + msg);
|
||||
|
||||
// Stringify to escape JSON characters and remove surrounding double quotes.
|
||||
const stringified = JSON.stringify(output);
|
||||
await user.send(stringified.substr(1, stringified.length - 2));
|
||||
await user.send(output);
|
||||
|
||||
resolve();
|
||||
}));
|
||||
@@ -53,5 +50,5 @@ const echoContract = async (ctx) => {
|
||||
// }
|
||||
}
|
||||
|
||||
const hpc = new HotPocketContract();
|
||||
const hpc = new HotPocket.Contract();
|
||||
hpc.init(echoContract);
|
||||
@@ -1,4 +1,4 @@
|
||||
const { HotPocketContract } = require("./hp-contract-lib");
|
||||
const HotPocket = require("./hp-contract-lib");
|
||||
const fs = require('fs');
|
||||
const bson = require('bson');
|
||||
|
||||
@@ -76,5 +76,5 @@ const fileContract = async (ctx) => {
|
||||
}
|
||||
};
|
||||
|
||||
const hpc = new HotPocketContract();
|
||||
hpc.init(fileContract);
|
||||
const hpc = new HotPocket.Contract();
|
||||
hpc.init(fileContract, HotPocket.clientProtocols.bson);
|
||||
|
||||
@@ -3,21 +3,31 @@ const tty = require('tty');
|
||||
require('process');
|
||||
|
||||
const MAX_SEQ_PACKET_SIZE = 128 * 1024;
|
||||
const CONTROL_MESSAGE = {
|
||||
CONTRACT_END: "contract_end",
|
||||
UNL_CHANGESET: "unl_changeset"
|
||||
const controlMessages = {
|
||||
contractEnd: "contract_end",
|
||||
unlChangeset: "unl_changeset"
|
||||
}
|
||||
Object.freeze(CONTROL_MESSAGE);
|
||||
Object.freeze(controlMessages);
|
||||
|
||||
const clientProtocols = {
|
||||
json: "json",
|
||||
bson: "bson"
|
||||
}
|
||||
Object.freeze(clientProtocols);
|
||||
|
||||
class HotPocketContract {
|
||||
|
||||
#controlChannel = null;
|
||||
#clientProtocol = null;
|
||||
|
||||
init(contractFunc) {
|
||||
init(contractFunc, clientProtocol = clientProtocols.json) {
|
||||
|
||||
if (this.#controlChannel) // Already initialized.
|
||||
return false;
|
||||
|
||||
this.#clientProtocol = clientProtocol;
|
||||
|
||||
// Check whether we are running on a console and provide error.
|
||||
if (tty.isatty(process.stdin.fd)) {
|
||||
console.error("Error: Hot Pocket smart contracts must be executed via Hot Pocket.");
|
||||
return false;
|
||||
@@ -37,7 +47,7 @@ class HotPocketContract {
|
||||
const pendingTasks = [];
|
||||
const nplChannel = new NplChannel(hpargs.nplfd);
|
||||
|
||||
const users = new UsersCollection(hpargs.userinfd, hpargs.users);
|
||||
const users = new UsersCollection(hpargs.userinfd, hpargs.users, this.#clientProtocol);
|
||||
const peers = new PeersCollection(hpargs.readonly, hpargs.unl, nplChannel, pendingTasks);
|
||||
const executionContext = new ContractExecutionContext(hpargs, users, peers, this.#controlChannel);
|
||||
|
||||
@@ -51,7 +61,7 @@ class HotPocketContract {
|
||||
}
|
||||
|
||||
#terminate = () => {
|
||||
this.#controlChannel.send({ type: CONTROL_MESSAGE.CONTRACT_END });
|
||||
this.#controlChannel.send({ type: controlMessages.contractEnd });
|
||||
this.#controlChannel.close();
|
||||
}
|
||||
}
|
||||
@@ -72,7 +82,7 @@ class ContractExecutionContext {
|
||||
async updateUnl(addArray, removeArray) {
|
||||
if (this.readonly)
|
||||
throw "UNL update not allowed in readonly mode."
|
||||
await this.#controlChannel.send({ type: CONTROL_MESSAGE.UNL_CHANGESET, add: addArray, remove: removeArray });
|
||||
await this.#controlChannel.send({ type: controlMessages.unlChangeset, add: addArray, remove: removeArray });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +91,7 @@ class UsersCollection {
|
||||
#users = {};
|
||||
#infd = null;
|
||||
|
||||
constructor(userInputsFd, usersObj) {
|
||||
constructor(userInputsFd, usersObj, clientProtocol) {
|
||||
this.#infd = userInputsFd;
|
||||
|
||||
Object.entries(usersObj).forEach(([pubKey, arr]) => {
|
||||
@@ -89,7 +99,7 @@ class UsersCollection {
|
||||
const outfd = arr[0]; // First array element is the output fd.
|
||||
arr.splice(0, 1); // Remove first element (output fd). The rest are pairs of msg offset/length tuples.
|
||||
|
||||
const channel = new UserChannel(outfd);
|
||||
const channel = new UserChannel(outfd, clientProtocol);
|
||||
this.#users[pubKey] = new User(pubKey, channel, arr);
|
||||
});
|
||||
}
|
||||
@@ -134,18 +144,41 @@ class User {
|
||||
|
||||
class UserChannel {
|
||||
#outfd = -1;
|
||||
#clientProtocol = null;
|
||||
|
||||
constructor(outfd) {
|
||||
constructor(outfd, clientProtocol) {
|
||||
this.#outfd = outfd;
|
||||
this.#clientProtocol = clientProtocol;
|
||||
}
|
||||
|
||||
send(msg) {
|
||||
const messageBuf = Buffer.from(msg);
|
||||
const messageBuf = this.serialize(msg);
|
||||
let headerBuf = Buffer.alloc(4);
|
||||
// Writing message length in big endian format.
|
||||
headerBuf.writeUInt32BE(messageBuf.byteLength)
|
||||
return writevAsync(this.#outfd, [headerBuf, messageBuf]);
|
||||
}
|
||||
|
||||
serialize(msg) {
|
||||
|
||||
if (!msg)
|
||||
throw "Cannot serialize null content.";
|
||||
|
||||
if (Buffer.isBuffer(msg))
|
||||
return msg;
|
||||
|
||||
if (this.#clientProtocol == clientProtocols.bson) {
|
||||
return Buffer.from(msg);
|
||||
}
|
||||
else { // json
|
||||
|
||||
// In JSON, we need to ensure that the final buffer contains a string.
|
||||
if (typeof msg === "string" || msg instanceof String)
|
||||
return Buffer.from(msg);
|
||||
else
|
||||
return Buffer.from(JSON.stringify(msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PeersCollection {
|
||||
@@ -299,5 +332,6 @@ const invokeCallback = async (callback, ...args) => {
|
||||
const errHandler = (err) => console.log(err);
|
||||
|
||||
module.exports = {
|
||||
HotPocketContract
|
||||
Contract: HotPocketContract,
|
||||
clientProtocols
|
||||
}
|
||||
44
src/conf.cpp
44
src/conf.cpp
@@ -96,13 +96,9 @@ namespace conf
|
||||
crypto::generate_signing_keys(cfg.pubkey, cfg.seckey);
|
||||
binpair_to_hex(cfg);
|
||||
|
||||
// Generate contract id hex.
|
||||
std::string rand_string;
|
||||
crypto::random_bytes(rand_string, 16);
|
||||
util::bin2hex(
|
||||
cfg.contractid,
|
||||
reinterpret_cast<const unsigned char *>(rand_string.data()),
|
||||
rand_string.length());
|
||||
cfg.hpversion = util::HP_VERSION;
|
||||
cfg.contractversion = "1.0";
|
||||
cfg.contractid = crypto::generate_uuid();
|
||||
|
||||
//Add self pubkey to the unl.
|
||||
cfg.unl.emplace(cfg.pubkey);
|
||||
@@ -221,21 +217,21 @@ namespace conf
|
||||
}
|
||||
ifs.close();
|
||||
|
||||
// Check whether the contract version is specified.
|
||||
std::string_view cfgversion = d["version"].as<std::string_view>();
|
||||
if (cfgversion.empty())
|
||||
// Check whether the hp version is specified.
|
||||
cfg.hpversion = d["hpversion"].as<std::string>();
|
||||
if (cfg.hpversion.empty())
|
||||
{
|
||||
std::cerr << "Contract config version missing.\n";
|
||||
std::cerr << "Contract config HP version missing.\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check whether this contract complies with the min version requirement.
|
||||
int verresult = util::version_compare(std::string(cfgversion), std::string(util::MIN_CONTRACT_VERSION));
|
||||
// Check whether this config complies with the min version requirement.
|
||||
int verresult = util::version_compare(cfg.hpversion, std::string(util::MIN_CONFIG_VERSION));
|
||||
if (verresult == -1)
|
||||
{
|
||||
std::cerr << "Contract version too old. Minimum "
|
||||
<< util::MIN_CONTRACT_VERSION << " required. "
|
||||
<< cfgversion << " found.\n";
|
||||
std::cerr << "Config version too old. Minimum "
|
||||
<< util::MIN_CONFIG_VERSION << " required. "
|
||||
<< cfg.hpversion << " found.\n";
|
||||
return -1;
|
||||
}
|
||||
else if (verresult == -2)
|
||||
@@ -244,9 +240,18 @@ namespace conf
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Load up the values into the struct.
|
||||
|
||||
cfg.contractid = d["contractid"].as<std::string>();
|
||||
cfg.contractversion = d["contractversion"].as<std::string>();
|
||||
if (cfg.contractid.empty())
|
||||
{
|
||||
std::cerr << "Contract id not specified.\n";
|
||||
return -1;
|
||||
}
|
||||
else if (cfg.contractversion.empty())
|
||||
{
|
||||
std::cerr << "Contract version not specified.\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (d["mode"] == MODE_OBSERVER)
|
||||
cfg.operating_mode = OPERATING_MODE::OBSERVER;
|
||||
@@ -372,8 +377,9 @@ namespace conf
|
||||
// Popualte json document with 'cfg' values.
|
||||
// ojson is used instead of json to preserve insertion order.
|
||||
jsoncons::ojson d;
|
||||
d.insert_or_assign("version", util::HP_VERSION);
|
||||
d.insert_or_assign("hpversion", cfg.hpversion);
|
||||
d.insert_or_assign("contractid", cfg.contractid);
|
||||
d.insert_or_assign("contractversion", cfg.contractversion);
|
||||
d.insert_or_assign("mode", cfg.operating_mode == OPERATING_MODE::OBSERVER ? MODE_OBSERVER : MODE_PROPOSER);
|
||||
|
||||
d.insert_or_assign("pubkeyhex", cfg.pubkeyhex);
|
||||
|
||||
@@ -84,7 +84,9 @@ namespace conf
|
||||
bool is_unl = false; // Indicate whether we are a unl node or not.
|
||||
|
||||
// Config elements which are loaded from the config file.
|
||||
std::string hpversion; // Version of Hot Pocket that generated the config.
|
||||
std::string contractid; // Contract guid.
|
||||
std::string contractversion; // Contract version string.
|
||||
OPERATING_MODE operating_mode = OPERATING_MODE::OBSERVER; // Configured startup operating mode of the contract (Observer/Proposer).
|
||||
std::string pubkeyhex; // Contract hex public key
|
||||
std::string seckeyhex; // Contract hex secret key
|
||||
|
||||
@@ -227,4 +227,23 @@ namespace crypto
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::string generate_uuid()
|
||||
{
|
||||
std::string rand_bytes;
|
||||
random_bytes(rand_bytes, 16);
|
||||
|
||||
// Set bits for UUID v4 variant 1.
|
||||
uint8_t *uuid = (uint8_t *)rand_bytes.data();
|
||||
uuid[6] = (uuid[8] & 0x0F) | 0x40;
|
||||
uuid[8] = (uuid[8] & 0xBF) | 0x80;
|
||||
|
||||
std::string hex;
|
||||
util::bin2hex(
|
||||
hex,
|
||||
reinterpret_cast<const unsigned char *>(rand_bytes.data()),
|
||||
rand_bytes.length());
|
||||
|
||||
return hex.substr(0, 8) + "-" + hex.substr(8, 4) + "-" + hex.substr(12, 4) + "-" + hex.substr(16, 4) + "-" + hex.substr(20);
|
||||
}
|
||||
|
||||
} // namespace crypto
|
||||
@@ -39,6 +39,8 @@ namespace crypto
|
||||
|
||||
std::string get_hash(const std::vector<std::string_view> &sw_vect);
|
||||
|
||||
std::string generate_uuid();
|
||||
|
||||
} // namespace crypto
|
||||
|
||||
#endif
|
||||
@@ -195,9 +195,11 @@ int main(int argc, char **argv)
|
||||
|
||||
hplog::init();
|
||||
|
||||
LOG_INFO << "Hot Pocket " << util::HP_VERSION;
|
||||
LOG_INFO << "Operating mode: "
|
||||
<< (conf::cfg.operating_mode == conf::OPERATING_MODE::OBSERVER ? "Observer" : "Proposer");
|
||||
LOG_INFO << "Public key: " << conf::cfg.pubkeyhex.substr(2); // Public key without 'ed' prefix.
|
||||
LOG_INFO << "Contract: " << conf::cfg.contractid << " (" << conf::cfg.contractversion << ")";
|
||||
|
||||
if (ledger::init() == -1 ||
|
||||
unl::init() == -1 ||
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
namespace msg::usrmsg::bson
|
||||
{
|
||||
|
||||
void create_user_challenge(std::vector<uint8_t> &msg, std::string &challengehex);
|
||||
|
||||
void create_status_response(std::vector<uint8_t> &msg, const uint64_t lcl_seq_no, std::string_view lcl);
|
||||
|
||||
void create_contract_input_status(std::vector<uint8_t> &msg, std::string_view status, std::string_view reason,
|
||||
|
||||
@@ -29,41 +29,84 @@ namespace msg::usrmsg::json
|
||||
* @param msg String reference to copy the generated json message string into.
|
||||
* Message format:
|
||||
* {
|
||||
* "type": "handshake_challenge",
|
||||
* "hp_version": "<hp protocol version>",
|
||||
* "type": "user_challenge",
|
||||
* "contract_id": "<contract id>",
|
||||
* "challenge": "<hex challenge string>"
|
||||
* "contract_version": "<contract version string>",
|
||||
* "challenge": "<challenge string>"
|
||||
* }
|
||||
* @param challengehex String reference to copy the generated hex challenge string into.
|
||||
* @param challenge_bytes String reference to copy the generated challenge bytes into.
|
||||
*/
|
||||
void create_user_challenge(std::vector<uint8_t> &msg, std::string &challengehex)
|
||||
void create_user_challenge(std::vector<uint8_t> &msg, std::string &challenge)
|
||||
{
|
||||
// Use libsodium to generate the random challenge bytes.
|
||||
unsigned char challenge_bytes[msg::usrmsg::CHALLENGE_LEN];
|
||||
randombytes_buf(challenge_bytes, msg::usrmsg::CHALLENGE_LEN);
|
||||
|
||||
// We pass the hex challenge string separately to the caller even though
|
||||
// we also include it in the challenge msg as well.
|
||||
|
||||
util::bin2hex(challengehex, challenge_bytes, msg::usrmsg::CHALLENGE_LEN);
|
||||
std::string challenge_bytes;
|
||||
crypto::random_bytes(challenge_bytes, msg::usrmsg::CHALLENGE_LEN);
|
||||
util::bin2hex(challenge,
|
||||
reinterpret_cast<const unsigned char *>(challenge_bytes.data()),
|
||||
msg::usrmsg::CHALLENGE_LEN);
|
||||
|
||||
// Construct the challenge msg json.
|
||||
// We do not use jasoncons library here in favour of performance because this is a simple json message.
|
||||
// We do not use jsoncons library here in favour of performance because this is a simple json message.
|
||||
|
||||
// Since we know the rough size of the challenge message we reserve adequate amount for the holder.
|
||||
// Only Hot Pocket version number is variable length.
|
||||
msg.reserve(256);
|
||||
msg += "{\"";
|
||||
msg += msg::usrmsg::FLD_HP_VERSION;
|
||||
msg += SEP_COLON;
|
||||
msg += msg::usrmsg::USER_PROTOCOL_VERSION;
|
||||
msg += SEP_COMMA;
|
||||
msg += msg::usrmsg::FLD_TYPE;
|
||||
msg += SEP_COLON;
|
||||
msg += msg::usrmsg::MSGTYPE_HANDSHAKE_CHALLENGE;
|
||||
msg += msg::usrmsg::MSGTYPE_USER_CHALLENGE;
|
||||
msg += SEP_COMMA;
|
||||
msg += msg::usrmsg::FLD_CONTRACT_ID;
|
||||
msg += SEP_COLON;
|
||||
msg += conf::cfg.contractid;
|
||||
msg += SEP_COMMA;
|
||||
msg += msg::usrmsg::FLD_CONTRACT_VERSION;
|
||||
msg += SEP_COLON;
|
||||
msg += conf::cfg.contractversion;
|
||||
msg += SEP_COMMA;
|
||||
msg += msg::usrmsg::FLD_CHALLENGE;
|
||||
msg += SEP_COLON;
|
||||
msg += challengehex;
|
||||
msg += challenge;
|
||||
msg += "\"}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs server challenge response message json. This gets sent when we receive
|
||||
* a challenge from the user.
|
||||
*
|
||||
* @param msg String reference to copy the generated json message string into.
|
||||
* Message format:
|
||||
* {
|
||||
* "type": "server_challenge_response",
|
||||
* "sig": "<hex encoded signature of the [challenge + contract_id]>",
|
||||
* "pubkey": "<our public key in hex>"
|
||||
* }
|
||||
* @param original_challenge Original challenge issued by the user.
|
||||
*/
|
||||
void create_server_challenge_response(std::vector<uint8_t> &msg, const std::string &original_challenge)
|
||||
{
|
||||
// Generate signature of challenge + contract id + contract version.
|
||||
const std::string content = original_challenge + conf::cfg.contractid + conf::cfg.contractversion;
|
||||
const std::string sig_hex = crypto::sign_hex(content, conf::cfg.seckeyhex);
|
||||
|
||||
// Since we know the rough size of the challenge message we reserve adequate amount for the holder.
|
||||
msg.reserve(256);
|
||||
msg += "{\"";
|
||||
msg += msg::usrmsg::FLD_TYPE;
|
||||
msg += SEP_COLON;
|
||||
msg += msg::usrmsg::MSGTYPE_SERVER_CHALLENGE_RESPONSE;
|
||||
msg += SEP_COMMA;
|
||||
msg += msg::usrmsg::FLD_SIG;
|
||||
msg += SEP_COLON;
|
||||
msg += sig_hex;
|
||||
msg += SEP_COMMA;
|
||||
msg += msg::usrmsg::FLD_PUBKEY;
|
||||
msg += SEP_COLON;
|
||||
msg += conf::cfg.pubkeyhex;
|
||||
msg += "\"}";
|
||||
}
|
||||
|
||||
@@ -146,16 +189,33 @@ namespace msg::usrmsg::json
|
||||
*/
|
||||
void create_contract_read_response_container(std::vector<uint8_t> &msg, std::string_view content)
|
||||
{
|
||||
msg.reserve(256);
|
||||
msg.reserve(content.size() + 256);
|
||||
msg += "{\"";
|
||||
msg += msg::usrmsg::FLD_TYPE;
|
||||
msg += SEP_COLON;
|
||||
msg += msg::usrmsg::MSGTYPE_CONTRACT_READ_RESPONSE;
|
||||
msg += SEP_COMMA;
|
||||
msg += msg::usrmsg::FLD_CONTENT;
|
||||
msg += SEP_COLON;
|
||||
msg += content;
|
||||
msg += "\"}";
|
||||
msg += SEP_COLON_NOQUOTE;
|
||||
|
||||
if (is_json_string(content))
|
||||
{
|
||||
// Process the final string using jsoncons.
|
||||
jsoncons::json jstring = content;
|
||||
jsoncons::json_options options;
|
||||
options.escape_all_non_ascii(true);
|
||||
|
||||
std::string escaped_content;
|
||||
jstring.dump(escaped_content);
|
||||
|
||||
msg += escaped_content;
|
||||
}
|
||||
else
|
||||
{
|
||||
msg += content;
|
||||
}
|
||||
|
||||
msg += "}";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,7 +232,9 @@ namespace msg::usrmsg::json
|
||||
*/
|
||||
void create_contract_output_container(std::vector<uint8_t> &msg, std::string_view content, const uint64_t lcl_seq_no, std::string_view lcl)
|
||||
{
|
||||
msg.reserve(256);
|
||||
const bool is_string = is_json_string(content);
|
||||
|
||||
msg.reserve(content.size() + 256);
|
||||
msg += "{\"";
|
||||
msg += msg::usrmsg::FLD_TYPE;
|
||||
msg += SEP_COLON;
|
||||
@@ -187,9 +249,26 @@ namespace msg::usrmsg::json
|
||||
msg += std::to_string(lcl_seq_no);
|
||||
msg += SEP_COMMA_NOQUOTE;
|
||||
msg += msg::usrmsg::FLD_CONTENT;
|
||||
msg += SEP_COLON;
|
||||
msg += content;
|
||||
msg += "\"}";
|
||||
msg += SEP_COLON_NOQUOTE;
|
||||
|
||||
if (is_json_string(content))
|
||||
{
|
||||
// Process the final string using jsoncons.
|
||||
jsoncons::json jstring = content;
|
||||
jsoncons::json_options options;
|
||||
options.escape_all_non_ascii(true);
|
||||
|
||||
std::string escaped_content;
|
||||
jstring.dump(escaped_content);
|
||||
|
||||
msg += escaped_content;
|
||||
}
|
||||
else
|
||||
{
|
||||
msg += content;
|
||||
}
|
||||
|
||||
msg += "}";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,80 +277,107 @@ namespace msg::usrmsg::json
|
||||
*
|
||||
* @param extracted_pubkeyhex The hex public key extracted from the response.
|
||||
* @param extracted_protocol The protocol code extracted from the response.
|
||||
* @param extracted_server_challenge Any server challenge issued by user.
|
||||
* @param response The response bytes to verify. This will be parsed as json.
|
||||
* Accepted response format:
|
||||
* {
|
||||
* "type": "handshake_response",
|
||||
* "challenge": "<original hex challenge the user received>",
|
||||
* "type": "user_challenge_response",
|
||||
* "sig": "<hex signature of the challenge>",
|
||||
* "pubkey": "<hex public key of the user>",
|
||||
* "server_challenge": "<hex encoded challenge issued to server>", (max 16 bytes/32 chars)
|
||||
* "protocol": "<json | bson>"
|
||||
* }
|
||||
* @param original_challenge The original hex challenge string issued to the user.
|
||||
* @param original_challenge The original challenge string we issued to the user.
|
||||
* @return 0 if challenge response is verified. -1 if challenge not met or an error occurs.
|
||||
*/
|
||||
int verify_user_handshake_response(std::string &extracted_pubkeyhex, std::string &extracted_protocol,
|
||||
std::string_view response, std::string_view original_challenge)
|
||||
int verify_user_challenge(std::string &extracted_pubkeyhex, std::string &extracted_protocol, std::string &extracted_server_challenge,
|
||||
std::string_view response, std::string_view original_challenge)
|
||||
{
|
||||
jsoncons::json d;
|
||||
if (parse_user_message(d, response) != 0)
|
||||
return -1;
|
||||
|
||||
// Validate msg type.
|
||||
if (d[msg::usrmsg::FLD_TYPE] != msg::usrmsg::MSGTYPE_HANDSHAKE_RESPONSE)
|
||||
if (d[msg::usrmsg::FLD_TYPE] != msg::usrmsg::MSGTYPE_USER_CHALLENGE_RESPONSE)
|
||||
{
|
||||
LOG_DEBUG << "User handshake response type invalid. 'handshake_response' expected.";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Compare the response handshake string with the original issued challenge.
|
||||
if (!d.contains(msg::usrmsg::FLD_CHALLENGE) || d[msg::usrmsg::FLD_CHALLENGE] != original_challenge.data())
|
||||
{
|
||||
LOG_DEBUG << "User handshake response 'challenge' invalid.";
|
||||
LOG_DEBUG << "User challenge response type invalid. 'handshake_response' expected.";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check for the 'sig' field existence.
|
||||
if (!d.contains(msg::usrmsg::FLD_SIG) || !d[msg::usrmsg::FLD_SIG].is<std::string>())
|
||||
{
|
||||
LOG_DEBUG << "User handshake response 'challenge signature' invalid.";
|
||||
LOG_DEBUG << "User challenge response 'challenge signature' invalid.";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check for the 'pubkey' field existence.
|
||||
if (!d.contains(msg::usrmsg::FLD_PUBKEY) || !d[msg::usrmsg::FLD_PUBKEY].is<std::string>())
|
||||
{
|
||||
LOG_DEBUG << "User handshake response 'public key' invalid.";
|
||||
LOG_DEBUG << "User challenge response 'public key' invalid.";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check for optional server challenge field existence and valid value.
|
||||
if (d.contains(msg::usrmsg::FLD_SERVER_CHALLENGE))
|
||||
{
|
||||
bool server_challenge_valid = false;
|
||||
|
||||
if (d[msg::usrmsg::FLD_SERVER_CHALLENGE].is<std::string>())
|
||||
{
|
||||
std::string_view challenge = d[msg::usrmsg::FLD_SERVER_CHALLENGE].as<std::string_view>();
|
||||
|
||||
if (!challenge.empty() && challenge.size() <= 32)
|
||||
{
|
||||
server_challenge_valid = true;
|
||||
extracted_server_challenge = challenge;
|
||||
}
|
||||
}
|
||||
|
||||
if (!server_challenge_valid)
|
||||
{
|
||||
LOG_DEBUG << "User challenge response 'server_challenge' invalid.";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for protocol field existence and valid value.
|
||||
if (!d.contains(msg::usrmsg::FLD_PROTOCOL) || !d[msg::usrmsg::FLD_PROTOCOL].is<std::string>())
|
||||
{
|
||||
|
||||
LOG_DEBUG << "User handshake response 'protocol' invalid.";
|
||||
LOG_DEBUG << "User challenge response 'protocol' invalid.";
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string_view protocolsv = d[msg::usrmsg::FLD_PROTOCOL].as<std::string_view>();
|
||||
if (protocolsv != "json" && protocolsv != "bson")
|
||||
{
|
||||
LOG_DEBUG << "User handshake response 'protocol' type invalid.";
|
||||
LOG_DEBUG << "User challenge response 'protocol' type invalid.";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Verify the challenge signature. We do this last due to signature verification cost.
|
||||
std::string_view pubkeysv = d[msg::usrmsg::FLD_PUBKEY].as<std::string_view>();
|
||||
if (crypto::verify_hex(
|
||||
original_challenge,
|
||||
d[msg::usrmsg::FLD_SIG].as<std::string_view>(),
|
||||
pubkeysv) != 0)
|
||||
|
||||
std::string_view pubkey_hex = d[msg::usrmsg::FLD_PUBKEY].as<std::string_view>();
|
||||
std::string pubkey_bytes;
|
||||
pubkey_bytes.resize(crypto::PFXD_PUBKEY_BYTES);
|
||||
util::hex2bin(reinterpret_cast<unsigned char *>(pubkey_bytes.data()),
|
||||
pubkey_bytes.size(),
|
||||
pubkey_hex);
|
||||
|
||||
std::string_view sig_hex = d[msg::usrmsg::FLD_SIG].as<std::string_view>();
|
||||
std::string sig_bytes;
|
||||
sig_bytes.resize(sig_hex.size() / 2);
|
||||
util::hex2bin(reinterpret_cast<unsigned char *>(sig_bytes.data()),
|
||||
sig_bytes.size(),
|
||||
sig_hex);
|
||||
|
||||
if (crypto::verify(original_challenge, sig_bytes, pubkey_bytes) != 0)
|
||||
{
|
||||
LOG_DEBUG << "User challenge response signature verification failed.";
|
||||
return -1;
|
||||
}
|
||||
|
||||
extracted_pubkeyhex = pubkeysv;
|
||||
extracted_pubkeyhex = pubkey_hex;
|
||||
extracted_protocol = protocolsv;
|
||||
|
||||
return 0;
|
||||
@@ -436,4 +542,31 @@ namespace msg::usrmsg::json
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool is_json_string(std::string_view content)
|
||||
{
|
||||
if (content.empty())
|
||||
return true;
|
||||
|
||||
const char first = content[0];
|
||||
const char last = content[content.size() - 1];
|
||||
|
||||
if ((first == '\"' && last == '\"') ||
|
||||
(first == '{' && last == '}') ||
|
||||
(first == '[' && last == ']') ||
|
||||
content == "true" || content == "false")
|
||||
return false;
|
||||
|
||||
// Check whether all characters are digits.
|
||||
bool decimal_found = false;
|
||||
for (const char c : content)
|
||||
{
|
||||
if ((c != '.' && (c < '0' || c > '9')) || (c == '.' && decimal_found)) // Not a number.
|
||||
return true;
|
||||
else if (c == '.') // There can only be one decimal in a proper number.
|
||||
decimal_found = true;
|
||||
}
|
||||
|
||||
return false; // Is a number.
|
||||
}
|
||||
|
||||
} // namespace msg::usrmsg::json
|
||||
@@ -6,7 +6,9 @@
|
||||
namespace msg::usrmsg::json
|
||||
{
|
||||
|
||||
void create_user_challenge(std::vector<uint8_t> &msg, std::string &challengehex);
|
||||
void create_user_challenge(std::vector<uint8_t> &msg, std::string &challenge);
|
||||
|
||||
void create_server_challenge_response(std::vector<uint8_t> &msg, const std::string &original_challenge);
|
||||
|
||||
void create_status_response(std::vector<uint8_t> &msg, const uint64_t lcl_seq_no, std::string_view lcl);
|
||||
|
||||
@@ -17,8 +19,8 @@ namespace msg::usrmsg::json
|
||||
|
||||
void create_contract_output_container(std::vector<uint8_t> &msg, std::string_view content, const uint64_t lcl_seq_no, std::string_view lcl);
|
||||
|
||||
int verify_user_handshake_response(std::string &extracted_pubkeyhex, std::string &extracted_protocol,
|
||||
std::string_view response, std::string_view original_challenge);
|
||||
int verify_user_challenge(std::string &extracted_pubkeyhex, std::string &extracted_protocol, std::string &extracted_server_challenge,
|
||||
std::string_view response, std::string_view original_challenge);
|
||||
|
||||
int parse_user_message(jsoncons::json &d, std::string_view message);
|
||||
|
||||
@@ -32,6 +34,8 @@ namespace msg::usrmsg::json
|
||||
int extract_input_container(std::string &input, std::string &nonce,
|
||||
uint64_t &max_lcl_seqno, std::string_view contentjson);
|
||||
|
||||
bool is_json_string(std::string_view content);
|
||||
|
||||
} // namespace msg::usrmsg::json
|
||||
|
||||
#endif
|
||||
@@ -7,10 +7,14 @@ namespace msg::usrmsg
|
||||
{
|
||||
// Length of user random challenge bytes.
|
||||
constexpr size_t CHALLENGE_LEN = 16;
|
||||
constexpr const char *USER_PROTOCOL_VERSION = "0.0";
|
||||
|
||||
// Message field names
|
||||
constexpr const char *FLD_HP_VERSION = "hp_version";
|
||||
constexpr const char *FLD_TYPE = "type";
|
||||
constexpr const char *FLD_SERVER_CHALLENGE = "server_challenge";
|
||||
constexpr const char *FLD_CONTRACT_ID = "contract_id";
|
||||
constexpr const char *FLD_CONTRACT_VERSION = "contract_version";
|
||||
constexpr const char *FLD_CHALLENGE = "challenge";
|
||||
constexpr const char *FLD_SIG = "sig";
|
||||
constexpr const char *FLD_PUBKEY = "pubkey";
|
||||
@@ -27,8 +31,9 @@ namespace msg::usrmsg
|
||||
constexpr const char *FLD_REASON = "reason";
|
||||
|
||||
// Message types
|
||||
constexpr const char *MSGTYPE_HANDSHAKE_CHALLENGE = "handshake_challenge";
|
||||
constexpr const char *MSGTYPE_HANDSHAKE_RESPONSE = "handshake_response";
|
||||
constexpr const char *MSGTYPE_USER_CHALLENGE = "user_challenge";
|
||||
constexpr const char *MSGTYPE_USER_CHALLENGE_RESPONSE = "user_challenge_response";
|
||||
constexpr const char *MSGTYPE_SERVER_CHALLENGE_RESPONSE = "server_challenge_response";
|
||||
constexpr const char *MSGTYPE_CONTRACT_READ_REQUEST = "contract_read_request";
|
||||
constexpr const char *MSGTYPE_CONTRACT_READ_RESPONSE = "contract_read_response";
|
||||
constexpr const char *MSGTYPE_CONTRACT_INPUT = "contract_input";
|
||||
|
||||
@@ -92,10 +92,17 @@ namespace usr
|
||||
|
||||
std::string user_pubkey_hex;
|
||||
std::string protocol_code;
|
||||
std::string_view original_challenge = session.issued_challenge;
|
||||
|
||||
if (msg::usrmsg::json::verify_user_handshake_response(user_pubkey_hex, protocol_code, message, original_challenge) == 0)
|
||||
std::string server_challenge;
|
||||
if (msg::usrmsg::json::verify_user_challenge(user_pubkey_hex, protocol_code, server_challenge, message, session.issued_challenge) == 0)
|
||||
{
|
||||
// If user has specified server challange, we need to send a challenge response.
|
||||
if (!server_challenge.empty())
|
||||
{
|
||||
std::vector<uint8_t> msg;
|
||||
msg::usrmsg::json::create_server_challenge_response(msg, server_challenge);
|
||||
session.send(msg);
|
||||
}
|
||||
|
||||
// Challenge signature verification successful. Add the user to our global user list.
|
||||
add_user(session, user_pubkey_hex, protocol_code);
|
||||
return 0;
|
||||
|
||||
@@ -15,8 +15,8 @@ namespace util
|
||||
// Hot Pocket version. Displayed on 'hotpocket version' and written to new contract configs.
|
||||
constexpr const char *HP_VERSION = "0.1";
|
||||
|
||||
// Minimum compatible contract config version (this will be used to validate contract configs)
|
||||
constexpr const char *MIN_CONTRACT_VERSION = "0.1";
|
||||
// Minimum compatible config version (this will be used to validate contract configs)
|
||||
constexpr const char *MIN_CONFIG_VERSION = "0.1";
|
||||
|
||||
// Current version of the peer message protocol.
|
||||
constexpr uint8_t PEERMSG_VERSION = 1;
|
||||
|
||||
@@ -84,7 +84,7 @@ do
|
||||
|
||||
# Update contract config.
|
||||
node -p "JSON.stringify({...require('./tmp.json'), \
|
||||
contractid: 'dummy', \
|
||||
contractid: '3c349abe-4d70-4f50-9fa6-018f1f2530ab', \
|
||||
binary: '$binary', \
|
||||
binargs: '$binargs', \
|
||||
appbill: '', \
|
||||
|
||||
@@ -364,7 +364,7 @@ do
|
||||
|
||||
# Merge json contents to produce final contract config.
|
||||
echo "$(cat ./cfg/node$n.cfg)" \
|
||||
'{"contractid":"dummy"}' \
|
||||
'{"contractid":"3c349abe-4d70-4f50-9fa6-018f1f2530ab"}' \
|
||||
'{"binary":"/usr/bin/node"}' \
|
||||
'{"binargs":"'$basedir'/hpfiles/nodejs_contract/echo_contract.js"}' \
|
||||
'{"peers":'${mypeers}'}' \
|
||||
|
||||
Reference in New Issue
Block a user