From a1e0fc3b88ed4305f942fadbea66b29a3934721c Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 16 Aug 2019 15:03:52 +0200 Subject: crypto worker refactoring --- src/crypto/browserWorkerEntry.ts | 120 ++++++++++++++++++++++++++++++++++++ src/crypto/cryptoApi-test.ts | 3 +- src/crypto/cryptoApi.ts | 44 +------------ src/crypto/cryptoWorker.ts | 81 ++---------------------- src/crypto/emscInterface-test.ts | 35 ++++++----- src/crypto/emscInterface.ts | 49 ++++++++++++++- src/crypto/emscLoader.d.ts | 64 ------------------- src/crypto/emscLoader.js | 117 ----------------------------------- src/crypto/nodeEmscriptenLoader.ts | 102 +++++++++++++++++++++++++++++++ src/crypto/nodeProcessWorker.ts | 64 ++++++++++++------- src/crypto/nodeWorkerEntry.ts | 105 +++++++++++++++---------------- src/crypto/synchronousWorker.ts | 122 ++++++++----------------------------- src/headless/taler-wallet-cli.ts | 2 +- 13 files changed, 420 insertions(+), 488 deletions(-) create mode 100644 src/crypto/browserWorkerEntry.ts delete mode 100644 src/crypto/emscLoader.d.ts delete mode 100644 src/crypto/emscLoader.js create mode 100644 src/crypto/nodeEmscriptenLoader.ts (limited to 'src') diff --git a/src/crypto/browserWorkerEntry.ts b/src/crypto/browserWorkerEntry.ts new file mode 100644 index 000000000..3df59fe0d --- /dev/null +++ b/src/crypto/browserWorkerEntry.ts @@ -0,0 +1,120 @@ +/* + This file is part of TALER + (C) 2016 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see + */ + +/** + * Web worker for crypto operations. + */ + +/** + * Imports. + */ + +import { CryptoImplementation } from "./cryptoImplementation"; +import { EmscEnvironment } from "./emscInterface"; + +const worker: Worker = (self as any) as Worker; + +class BrowserEmscriptenLoader { + private cachedEmscEnvironment: EmscEnvironment | undefined = undefined; + private cachedEmscEnvironmentPromise: + | Promise + | undefined = undefined; + + async getEmscriptenEnvironment(): Promise { + + if (this.cachedEmscEnvironment) { + return this.cachedEmscEnvironment; + } + + if (this.cachedEmscEnvironmentPromise) { + return this.cachedEmscEnvironmentPromise; + } + + console.log("loading emscripten lib with 'importScripts'"); + // @ts-ignore + self.TalerEmscriptenLib = {}; + // @ts-ignore + importScripts('/emscripten/taler-emscripten-lib.js') + // @ts-ignore + if (!self.TalerEmscriptenLib) { + throw Error("can't import taler emscripten lib"); + } + const locateFile = (path: string, scriptDir: string) => { + console.log("locating file", "path", path, "scriptDir", scriptDir); + // This is quite hacky and assumes that our scriptDir is dist/ + return scriptDir + "../emscripten/" + path; + }; + console.log("instantiating TalerEmscriptenLib"); + // @ts-ignore + const lib = self.TalerEmscriptenLib({ locateFile }); + return new Promise((resolve, reject) => { + lib.then((mod: any) => { + this.cachedEmscEnvironmentPromise = undefined; + const emsc = new EmscEnvironment(mod); + this.cachedEmscEnvironment = new EmscEnvironment(mod); + console.log("emscripten module fully loaded"); + resolve(emsc); + }); + }); + } +} + +let loader = new BrowserEmscriptenLoader(); + +async function handleRequest(operation: string, id: number, args: string[]) { + let emsc = await loader.getEmscriptenEnvironment(); + + const impl = new CryptoImplementation(emsc); + + if (!(operation in impl)) { + console.error(`crypto operation '${operation}' not found`); + return; + } + + try { + const result = (impl as any)[operation](...args); + worker.postMessage({ result, id }); + } catch (e) { + console.log("error during operation", e); + return; + } +} + +worker.onmessage = (msg: MessageEvent) => { + const args = msg.data.args; + if (!Array.isArray(args)) { + console.error("args must be array"); + return; + } + const id = msg.data.id; + if (typeof id !== "number") { + console.error("RPC id must be number"); + return; + } + const operation = msg.data.operation; + if (typeof operation !== "string") { + console.error("RPC operation must be string"); + return; + } + + if (CryptoImplementation.enableTracing) { + console.log("onmessage with", operation); + } + + handleRequest(operation, id, args).catch((e) => { + console.error("error in browsere worker", e); + }); +}; diff --git a/src/crypto/cryptoApi-test.ts b/src/crypto/cryptoApi-test.ts index 9c592412e..48231e5ff 100644 --- a/src/crypto/cryptoApi-test.ts +++ b/src/crypto/cryptoApi-test.ts @@ -24,7 +24,8 @@ import { ReserveRecord, } from "../dbTypes"; -import { CryptoApi, NodeCryptoWorkerFactory } from "./cryptoApi"; +import { CryptoApi } from "./cryptoApi"; +import { NodeCryptoWorkerFactory } from "./nodeProcessWorker"; const masterPub1: string = "CQQZ9DY3MZ1ARMN5K1VKDETS04Y2QCKMMCFHZSWJWWVN82BTTH00"; diff --git a/src/crypto/cryptoApi.ts b/src/crypto/cryptoApi.ts index c68b10700..a4e12c00f 100644 --- a/src/crypto/cryptoApi.ts +++ b/src/crypto/cryptoApi.ts @@ -34,6 +34,8 @@ import { WireFee, } from "../dbTypes"; +import { CryptoWorker } from "./cryptoWorker"; + import { ContractTerms, PaybackRequest } from "../talerTypes"; import { BenchmarkResult, CoinWithDenom, PayCoinInfo } from "../walletTypes"; @@ -83,15 +85,6 @@ interface WorkItem { */ const NUM_PRIO = 5; -interface CryptoWorker { - postMessage(message: any): void; - - terminate(): void; - - onmessage: (m: any) => void; - onerror: (m: any) => void; -} - export interface CryptoWorkerFactory { /** * Start a new worker. @@ -105,21 +98,6 @@ export interface CryptoWorkerFactory { getConcurrency(): number; } -export class NodeCryptoWorkerFactory implements CryptoWorkerFactory { - startWorker(): CryptoWorker { - if (typeof require === "undefined") { - throw Error("cannot make worker, require(...) not defined"); - } - const workerCtor = require("./nodeProcessWorker").Worker; - const workerPath = __dirname + "/cryptoWorker.js"; - return new workerCtor(workerPath); - } - - getConcurrency(): number { - return 2; - } -} - export class BrowserCryptoWorkerFactory implements CryptoWorkerFactory { startWorker(): CryptoWorker { const workerCtor = Worker; @@ -141,24 +119,6 @@ export class BrowserCryptoWorkerFactory implements CryptoWorkerFactory { } } -/** - * The synchronous crypto worker produced by this factory doesn't run in the - * background, but actually blocks the caller until the operation is done. - */ -export class SynchronousCryptoWorkerFactory implements CryptoWorkerFactory { - startWorker(): CryptoWorker { - if (typeof require === "undefined") { - throw Error("cannot make worker, require(...) not defined"); - } - const workerCtor = require("./synchronousWorker").SynchronousCryptoWorker; - return new workerCtor(); - } - - getConcurrency(): number { - return 1; - } -} - /** * Crypto API that interfaces manages a background crypto thread * for the execution of expensive operations. diff --git a/src/crypto/cryptoWorker.ts b/src/crypto/cryptoWorker.ts index 11e3d964c..0ea641dde 100644 --- a/src/crypto/cryptoWorker.ts +++ b/src/crypto/cryptoWorker.ts @@ -1,77 +1,8 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. +export interface CryptoWorker { + postMessage(message: any): void; - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. + terminate(): void; - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see - */ - -/** - * Web worker for crypto operations. - */ - -/** - * Imports. - */ - -import * as emscLoader from "./emscLoader"; - -import { CryptoImplementation } from "./cryptoImplementation"; -import { EmscEnvironment } from "./emscInterface"; - -const worker: Worker = (self as any) as Worker; - -let impl: CryptoImplementation | undefined; - - -worker.onmessage = (msg: MessageEvent) => { - const args = msg.data.args; - if (!Array.isArray(args)) { - console.error("args must be array"); - return; - } - const id = msg.data.id; - if (typeof id !== "number") { - console.error("RPC id must be number"); - return; - } - const operation = msg.data.operation; - if (typeof operation !== "string") { - console.error("RPC operation must be string"); - return; - } - - if (CryptoImplementation.enableTracing) { - console.log("onmessage with", operation); - } - - emscLoader.getLib().then(p => { - const lib = p.lib; - const emsc = new EmscEnvironment(lib); - const impl = new CryptoImplementation(emsc); - - if (!(operation in impl)) { - console.error(`unknown operation: '${operation}'`); - return; - } - - if (CryptoImplementation.enableTracing) { - console.log("about to execute", operation); - } - - const result = (impl as any)[operation](...args); - - if (CryptoImplementation.enableTracing) { - console.log("finished executing", operation); - } - worker.postMessage({ result, id }); - }); -}; + onmessage: (m: any) => void; + onerror: (m: any) => void; +} \ No newline at end of file diff --git a/src/crypto/emscInterface-test.ts b/src/crypto/emscInterface-test.ts index 51f2d58b6..b1a45cbcf 100644 --- a/src/crypto/emscInterface-test.ts +++ b/src/crypto/emscInterface-test.ts @@ -17,13 +17,13 @@ // tslint:disable:max-line-length import test from "ava"; -import * as emscLoader from "./emscLoader"; +import { NodeEmscriptenLoader } from "./nodeEmscriptenLoader"; import * as native from "./emscInterface"; test("string hashing", async (t) => { - const { lib } = await emscLoader.getLib(); - const emsc = new native.EmscEnvironment(lib); + const loader = new NodeEmscriptenLoader(); + const emsc = await loader.getEmscriptenEnvironment(); const x = native.ByteArray.fromStringWithNull(emsc, "hello taler"); const h = "8RDMADB3YNF3QZBS3V467YZVJAMC2QAQX0TZGVZ6Q5PFRRAJFT70HHN0QF661QR9QWKYMMC7YEMPD679D2RADXCYK8Y669A2A5MKQFR"; @@ -35,8 +35,8 @@ test("string hashing", async (t) => { test("signing", async (t) => { - const { lib } = await emscLoader.getLib(); - const emsc = new native.EmscEnvironment(lib); + const loader = new NodeEmscriptenLoader(); + const emsc = await loader.getEmscriptenEnvironment(); const x = native.ByteArray.fromStringWithNull(emsc, "hello taler"); const priv = native.EddsaPrivateKey.create(emsc, ); @@ -49,8 +49,8 @@ test("signing", async (t) => { test("signing-fixed-data", async (t) => { - const { lib } = await emscLoader.getLib(); - const emsc = new native.EmscEnvironment(lib); + const loader = new NodeEmscriptenLoader(); + const emsc = await loader.getEmscriptenEnvironment(); const x = native.ByteArray.fromStringWithNull(emsc, "hello taler"); const purpose = new native.EccSignaturePurpose(emsc, native.SignaturePurpose.TEST, x); @@ -74,8 +74,8 @@ const denomPubStr1 = "51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30G9R64VK6HHS6M test("rsa-encode", async (t) => { - const { lib } = await emscLoader.getLib(); - const emsc = new native.EmscEnvironment(lib); + const loader = new NodeEmscriptenLoader(); + const emsc = await loader.getEmscriptenEnvironment(); const pubHashStr = "JM63YM5X7X547164QJ3MGJZ4WDD47GEQR5DW5SH35G4JFZXEJBHE5JBNZM5K8XN5C4BRW25BE6GSVAYBF790G2BZZ13VW91D41S4DS0"; const denomPub = native.RsaPublicKey.fromCrock(emsc, denomPubStr1); @@ -86,8 +86,8 @@ test("rsa-encode", async (t) => { test("withdraw-request", async (t) => { - const { lib } = await emscLoader.getLib(); - const emsc = new native.EmscEnvironment(lib); + const loader = new NodeEmscriptenLoader(); + const emsc = await loader.getEmscriptenEnvironment(); const reservePrivStr = "G9R8KRRCAFKPD0KW7PW48CC2T03VQ8K2AN9J6J6K2YW27J5MHN90"; const reservePriv = native.EddsaPrivateKey.fromCrock(emsc, reservePrivStr); @@ -117,9 +117,8 @@ test("withdraw-request", async (t) => { test("currency-conversion", async (t) => { - - const { lib } = await emscLoader.getLib(); - const emsc = new native.EmscEnvironment(lib); + const loader = new NodeEmscriptenLoader(); + const emsc = await loader.getEmscriptenEnvironment(); const a1 = new native.Amount(emsc, {currency: "KUDOS", value: 1, fraction: 50000000}); const a2 = new native.Amount(emsc, {currency: "KUDOS", value: 1, fraction: 50000000}); @@ -133,8 +132,8 @@ test("currency-conversion", async (t) => { test("ecdsa", async (t) => { - const { lib } = await emscLoader.getLib(); - const emsc = new native.EmscEnvironment(lib); + const loader = new NodeEmscriptenLoader(); + const emsc = await loader.getEmscriptenEnvironment(); const priv = native.EcdsaPrivateKey.create(emsc); const pub1 = priv.getPublicKey(); @@ -145,8 +144,8 @@ test("ecdsa", async (t) => { test("ecdhe", async (t) => { - const { lib } = await emscLoader.getLib(); - const emsc = new native.EmscEnvironment(lib); + const loader = new NodeEmscriptenLoader(); + const emsc = await loader.getEmscriptenEnvironment(); const priv = native.EcdhePrivateKey.create(emsc); const pub = priv.getPublicKey(); diff --git a/src/crypto/emscInterface.ts b/src/crypto/emscInterface.ts index 96e94e3fb..0c7edefab 100644 --- a/src/crypto/emscInterface.ts +++ b/src/crypto/emscInterface.ts @@ -28,8 +28,6 @@ */ import { AmountJson } from "../amounts"; -import { EmscFunGen, EmscLib } from "./emscLoader"; - /** * Size of a native pointer. Must match the size * use when compiling via emscripten. @@ -38,6 +36,53 @@ const PTR_SIZE = 4; const GNUNET_OK = 1; + +/** + * Signature of the function that retrieves emscripten + * function implementations. + */ +export interface EmscFunGen { + (name: string, + ret: string, + args: string[]): ((...x: Array) => any); + (name: string, + ret: "number", + args: string[]): ((...x: Array) => number); + (name: string, + ret: "void", + args: string[]): ((...x: Array) => void); + (name: string, + ret: "string", + args: string[]): ((...x: Array) => string); +} + + +interface EmscLib { + cwrap: EmscFunGen; + + ccall(name: string, ret: "number"|"string", argTypes: any[], args: any[]): any; + + stringToUTF8(s: string, addr: number, maxLength: number): void; + + onRuntimeInitialized(f: () => void): void; + + readBinary?: (filename: string) => Promise; + + calledRun?: boolean; + + _free(ptr: number): void; + + _malloc(n: number): number; + + Pointer_stringify(p: number, len?: number): string; + + getValue(ptr: number, type: string, noSafe?: boolean): number; + + setValue(ptr: number, value: number, type: string, noSafe?: boolean): void; + + writeStringToMemory(s: string, buffer: number, dontAddNull?: boolean): void; +} + interface EmscFunctions { amount_add(a1: number, a2: number, a3: number): number; amount_cmp(a1: number, a2: number): number; diff --git a/src/crypto/emscLoader.d.ts b/src/crypto/emscLoader.d.ts deleted file mode 100644 index 3ec4f4cfb..000000000 --- a/src/crypto/emscLoader.d.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see - */ - - -declare function getLib(): Promise<{ lib: EmscLib }>; - -/** - * Signature of the function that retrieves emscripten - * function implementations. - */ -export interface EmscFunGen { - (name: string, - ret: string, - args: string[]): ((...x: Array) => any); - (name: string, - ret: "number", - args: string[]): ((...x: Array) => number); - (name: string, - ret: "void", - args: string[]): ((...x: Array) => void); - (name: string, - ret: "string", - args: string[]): ((...x: Array) => string); -} - - -interface EmscLib { - cwrap: EmscFunGen; - - ccall(name: string, ret: "number"|"string", argTypes: any[], args: any[]): any; - - stringToUTF8(s: string, addr: number, maxLength: number): void; - - onRuntimeInitialized(f: () => void): void; - - readBinary?: (filename: string) => Promise; - - calledRun?: boolean; - - _free(ptr: number): void; - - _malloc(n: number): number; - - Pointer_stringify(p: number, len?: number): string; - - getValue(ptr: number, type: string, noSafe?: boolean): number; - - setValue(ptr: number, value: number, type: string, noSafe?: boolean): void; - - writeStringToMemory(s: string, buffer: number, dontAddNull?: boolean): void; -} diff --git a/src/crypto/emscLoader.js b/src/crypto/emscLoader.js deleted file mode 100644 index 25dc6b85c..000000000 --- a/src/crypto/emscLoader.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - This file is part of TALER - (C) 2017 Inria and GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see - */ - - -// @ts-nocheck - - -/** - * This module loads the emscripten library, and is written in unchecked - * JavaScript since it needs to do environment detection and dynamically select - * the right way to load the library. - */ - -let cachedLib = undefined; -let cachedLibPromise = undefined; - -export let enableTracing = false; - -/** - * Load the taler emscripten lib. - * - * If in a WebWorker, importScripts is used. Inside a browser, the module must - * be globally available. Inside node, require is used. - * - * Returns a Promise<{ lib: EmscLib }> - */ -export function getLib() { - enableTracing && console.log("in getLib"); - if (cachedLib) { - enableTracing && console.log("lib is cached"); - return Promise.resolve({ lib: cachedLib }); - } - if (cachedLibPromise) { - return cachedLibPromise; - } - if (typeof require !== "undefined") { - enableTracing && console.log("trying to load emscripten lib with 'require'"); - // Make sure that TypeScript doesn't try - // to check the taler-emscripten-lib. - const indirectRequire = require; - const g = global; - // unavoidable hack, so that emscripten detects - // the environment as node even though importScripts - // is present. - const savedImportScripts = g.importScripts; - delete g.importScripts; - // Assume that the code is run from the build/ directory. - const libFn = indirectRequire("../../../emscripten/taler-emscripten-lib.js"); - const lib = libFn(); - g.importScripts = savedImportScripts; - if (lib) { - if (!lib.ccall) { - throw Error("sanity check failed: taler-emscripten lib does not have 'ccall'"); - } - cachedLibPromise = new Promise((resolve, reject) => { - lib.onRuntimeInitialized = () => { - cachedLib = lib; - cachedLibPromise = undefined; - resolve({ lib: cachedLib }); - }; - }); - return cachedLibPromise; - } else { - // When we're running as a webpack bundle, the above require might - // have failed and returned 'undefined', so we try other ways to import. - console.log("failed to load emscripten lib with 'require', trying alternatives"); - } - } - - if (typeof importScripts !== "undefined") { - console.log("trying to load emscripten lib with 'importScripts'"); - self.TalerEmscriptenLib = {}; - importScripts('/emscripten/taler-emscripten-lib.js') - if (!self.TalerEmscriptenLib) { - throw Error("can't import taler emscripten lib"); - } - const locateFile = (path, scriptDir) => { - console.log("locating file", "path", path, "scriptDir", scriptDir); - // This is quite hacky and assumes that our scriptDir is dist/ - return scriptDir + "../emscripten/" + path; - }; - console.log("instantiating TalerEmscriptenLib"); - const lib = self.TalerEmscriptenLib({ locateFile }); - cachedLib = lib; - return new Promise((resolve, reject) => { - lib.then(mod => { - console.log("emscripten module fully loaded"); - resolve({ lib: mod }); - }); - }); - } - - // Last resort, we don't have require, we're not running in a webworker. - // Maybe we're on a normal browser page, in this case TalerEmscriptenLib - // must be included in a script tag on the page. - - if (typeof window !== "undefined") { - if (window.TalerEmscriptenLib) { - return Promise.resolve(TalerEmscriptenLib); - } - throw Error("Looks like running in browser, but TalerEmscriptenLib is not defined"); - } - throw Error("Running in unsupported environment"); -} diff --git a/src/crypto/nodeEmscriptenLoader.ts b/src/crypto/nodeEmscriptenLoader.ts new file mode 100644 index 000000000..6d6a7306b --- /dev/null +++ b/src/crypto/nodeEmscriptenLoader.ts @@ -0,0 +1,102 @@ + +import { EmscEnvironment } from "./emscInterface"; +import { CryptoImplementation } from "./cryptoImplementation"; + +import fs = require("fs"); + +export class NodeEmscriptenLoader { + private cachedEmscEnvironment: EmscEnvironment | undefined = undefined; + private cachedEmscEnvironmentPromise: + | Promise + | undefined = undefined; + + private async getWasmBinary(): Promise { + // @ts-ignore + const akonoGetData = global.__akono_getData; + if (akonoGetData) { + // We're running embedded node on Android + console.log("reading wasm binary from akono"); + const data = akonoGetData("taler-emscripten-lib.wasm"); + // The data we get is base64-encoded binary data + let buf = new Buffer(data, 'base64'); + return new Uint8Array(buf); + + } else { + // We're in a normal node environment + const binaryPath = __dirname + "/../../../emscripten/taler-emscripten-lib.wasm"; + console.log("reading from", binaryPath); + const wasmBinary = new Uint8Array(fs.readFileSync(binaryPath)); + return wasmBinary; + } + } + + async getEmscriptenEnvironment(): Promise { + if (this.cachedEmscEnvironment) { + return this.cachedEmscEnvironment; + } + + if (this.cachedEmscEnvironmentPromise) { + return this.cachedEmscEnvironmentPromise; + } + + let lib: any; + + const wasmBinary = await this.getWasmBinary(); + + return new Promise((resolve, reject) => { + // Arguments passed to the emscripten prelude + const libArgs = { + wasmBinary, + onRuntimeInitialized: () => { + if (!lib) { + console.error("fatal emscripten initialization error"); + return; + } + this.cachedEmscEnvironmentPromise = undefined; + this.cachedEmscEnvironment = new EmscEnvironment(lib); + resolve(this.cachedEmscEnvironment); + }, + }; + + // Make sure that TypeScript doesn't try + // to check the taler-emscripten-lib. + const indirectRequire = require; + + const g = global; + + // unavoidable hack, so that emscripten detects + // the environment as node even though importScripts + // is present. + + // @ts-ignore + const savedImportScripts = g.importScripts; + // @ts-ignore + delete g.importScripts; + // @ts-ignore + const savedCrypto = g.crypto; + // @ts-ignore + delete g.crypto; + + // Assume that the code is run from the build/ directory. + const libFn = indirectRequire( + "../../../emscripten/taler-emscripten-lib.js", + ); + lib = libFn(libArgs); + + // @ts-ignore + g.importScripts = savedImportScripts; + // @ts-ignore + g.crypto = savedCrypto; + + if (!lib) { + throw Error("could not load taler-emscripten-lib.js"); + } + + if (!lib.ccall) { + throw Error( + "sanity check failed: taler-emscripten lib does not have 'ccall'", + ); + } + }); + } +} diff --git a/src/crypto/nodeProcessWorker.ts b/src/crypto/nodeProcessWorker.ts index b5a2e8b44..c5d0f2e71 100644 --- a/src/crypto/nodeProcessWorker.ts +++ b/src/crypto/nodeProcessWorker.ts @@ -1,3 +1,5 @@ +import { CryptoWorkerFactory } from "./cryptoApi"; + /* This file is part of TALER (C) 2016 GNUnet e.V. @@ -17,11 +19,29 @@ // tslint:disable:no-var-requires -const path = require("path"); -const fork = require("child_process").fork; +import { CryptoWorker } from "./cryptoWorker"; + +import path = require("path"); +import child_process = require("child_process"); const nodeWorkerEntry = path.join(__dirname, "nodeWorkerEntry.js"); + +export class NodeCryptoWorkerFactory implements CryptoWorkerFactory { + startWorker(): CryptoWorker { + if (typeof require === "undefined") { + throw Error("cannot make worker, require(...) not defined"); + } + const workerCtor = require("./nodeProcessWorker").Worker; + const workerPath = __dirname + "/cryptoWorker.js"; + return new workerCtor(workerPath); + } + + getConcurrency(): number { + return 4; + } +} + /** * Worker implementation that uses node subprocesses. */ @@ -38,33 +58,35 @@ export class Worker { */ onerror: undefined | ((m: any) => void); - constructor(scriptFilename: string) { - this.child = fork(nodeWorkerEntry); + private dispatchMessage(msg: any) { + if (this.onmessage) { + this.onmessage({ data: msg }); + } else { + console.warn("no handler for worker event 'message' defined") + } + } + + private dispatchError(msg: any) { + if (this.onerror) { + this.onerror({ data: msg }); + } else { + console.warn("no handler for worker event 'error' defined") + } + } + + constructor() { + this.child = child_process.fork(nodeWorkerEntry); this.onerror = undefined; this.onmessage = undefined; this.child.on("error", (e: any) => { - if (this.onerror) { - this.onerror(e); - } + this.dispatchError(e); }); this.child.on("message", (msg: any) => { - const message = JSON.parse(msg); - - if (!message.error && this.onmessage) { - this.onmessage(message); - } - - if (message.error && this.onerror) { - const error = new Error(message.error); - error.stack = message.stack; - - this.onerror(error); - } + console.log("nodeProcessWorker got child message", msg); + this.dispatchMessage(msg); }); - - this.child.send({scriptFilename, cwd: process.cwd()}); } /** diff --git a/src/crypto/nodeWorkerEntry.ts b/src/crypto/nodeWorkerEntry.ts index 9e2647ff6..1ed16f870 100644 --- a/src/crypto/nodeWorkerEntry.ts +++ b/src/crypto/nodeWorkerEntry.ts @@ -17,61 +17,64 @@ // tslint:disable:no-var-requires -const fs = require("fs"); -const vm = require("vm"); - -process.once("message", (obj: any) => { - const g: any = global as any; - - (g as any).self = { - addEventListener: (event: "error" | "message", fn: (x: any) => void) => { - if (event === "error") { - g.onerror = fn; - } else if (event === "message") { - g.onmessage = fn; - } - }, - close: () => { - process.exit(0); - }, - onerror: (err: any) => { - const str: string = JSON.stringify({error: err.message, stack: err.stack}); - if (process.send) { - process.send(str); - } - }, - onmessage: undefined, - postMessage: (msg: any) => { - const str: string = JSON.stringify({data: msg}); - if (process.send) { - process.send(str); - } - }, - }; - - g.__dirname = obj.cwd; - g.__filename = __filename; - g.importScripts = (...files: string[]) => { - if (files.length > 0) { - vm.createScript(files.map((file) => fs.readFileSync(file, "utf8")).join("\n")).runInThisContext(); - } - }; +import fs = require("fs"); +import vm = require("vm"); +import { NodeEmscriptenLoader } from "./nodeEmscriptenLoader"; +import { CryptoImplementation } from "./cryptoImplementation"; - Object.keys(g.self).forEach((key) => { - g[key] = g.self[key]; - }); +const loader = new NodeEmscriptenLoader(); + +async function handleRequest(operation: string, id: number, args: string[]) { + let emsc = await loader.getEmscriptenEnvironment(); - process.on("message", (msg: any) => { - try { - (g.onmessage || g.self.onmessage || (() => undefined))(JSON.parse(msg)); - } catch (err) { - (g.onerror || g.self.onerror || (() => undefined))(err); + const impl = new CryptoImplementation(emsc); + + if (!(operation in impl)) { + console.error(`crypto operation '${operation}' not found`); + return; + } + + try { + const result = (impl as any)[operation](...args); + if (process.send) { + process.send({ result, id }); + } else { + console.error("process.send not available"); } - }); + } catch (e) { + console.log("error during operation", e); + return; + } +} - process.on("uncaughtException", (err: any) => { - (g.onerror || g.self.onerror || (() => undefined))(err); +process.on("message", (msgStr: any) => { + console.log("got message in node worker entry", msgStr); + + console.log("typeof msg", typeof msgStr); + + const msg = JSON.parse(msgStr); + + const args = msg.data.args; + if (!Array.isArray(args)) { + console.error("args must be array"); + return; + } + const id = msg.data.id; + if (typeof id !== "number") { + console.error("RPC id must be number"); + return; + } + const operation = msg.data.operation; + if (typeof operation !== "string") { + console.error("RPC operation must be string"); + return; + } + + handleRequest(operation, id, args).catch((e) => { + console.error("error in node worker", e); }); +}); - require(obj.scriptFilename); +process.on("uncaughtException", (err: any) => { + console.log("uncaught exception in node worker entry", err); }); diff --git a/src/crypto/synchronousWorker.ts b/src/crypto/synchronousWorker.ts index 7d115f1dc..4369612ad 100644 --- a/src/crypto/synchronousWorker.ts +++ b/src/crypto/synchronousWorker.ts @@ -14,19 +14,37 @@ GNU Taler; see the file COPYING. If not, see */ -import { EmscEnvironment } from "./emscInterface"; import { CryptoImplementation } from "./cryptoImplementation"; +import { NodeEmscriptenLoader } from "./nodeEmscriptenLoader"; + import fs = require("fs"); +import { CryptoWorkerFactory } from "./cryptoApi"; +import { CryptoWorker } from "./cryptoWorker"; + +/** + * The synchronous crypto worker produced by this factory doesn't run in the + * background, but actually blocks the caller until the operation is done. + */ +export class SynchronousCryptoWorkerFactory implements CryptoWorkerFactory { + startWorker(): CryptoWorker { + if (typeof require === "undefined") { + throw Error("cannot make worker, require(...) not defined"); + } + const workerCtor = require("./synchronousWorker").SynchronousCryptoWorker; + return new workerCtor(); + } + + getConcurrency(): number { + return 1; + } +} + /** * Worker implementation that uses node subprocesses. */ export class SynchronousCryptoWorker { - private cachedEmscEnvironment: EmscEnvironment | undefined = undefined; - private cachedEmscEnvironmentPromise: - | Promise - | undefined = undefined; /** * Function to be called when we receive a message from the worker thread. @@ -38,6 +56,8 @@ export class SynchronousCryptoWorker { */ onerror: undefined | ((m: any) => void); + private emscriptenLoader = new NodeEmscriptenLoader(); + constructor() { this.onerror = undefined; this.onmessage = undefined; @@ -57,96 +77,6 @@ export class SynchronousCryptoWorker { } } - private async getWasmBinary(): Promise { - // @ts-ignore - const akonoGetData = global.__akono_getData; - if (akonoGetData) { - // We're running embedded node on Android - console.log("reading wasm binary from akono"); - const data = akonoGetData("taler-emscripten-lib.wasm"); - // The data we get is base64-encoded binary data - let buf = new Buffer(data, 'base64'); - return new Uint8Array(buf); - - } else { - // We're in a normal node environment - const binaryPath = __dirname + "/../../../emscripten/taler-emscripten-lib.wasm"; - console.log("reading from", binaryPath); - const wasmBinary = new Uint8Array(fs.readFileSync(binaryPath)); - return wasmBinary; - } - } - - private async getEmscriptenEnvironment(): Promise { - if (this.cachedEmscEnvironment) { - return this.cachedEmscEnvironment; - } - - if (this.cachedEmscEnvironmentPromise) { - return this.cachedEmscEnvironmentPromise; - } - - let lib: any; - - const wasmBinary = await this.getWasmBinary(); - - return new Promise((resolve, reject) => { - // Arguments passed to the emscripten prelude - const libArgs = { - wasmBinary, - onRuntimeInitialized: () => { - if (!lib) { - console.error("fatal emscripten initialization error"); - return; - } - this.cachedEmscEnvironmentPromise = undefined; - this.cachedEmscEnvironment = new EmscEnvironment(lib); - resolve(this.cachedEmscEnvironment); - }, - }; - - // Make sure that TypeScript doesn't try - // to check the taler-emscripten-lib. - const indirectRequire = require; - - const g = global; - - // unavoidable hack, so that emscripten detects - // the environment as node even though importScripts - // is present. - - // @ts-ignore - const savedImportScripts = g.importScripts; - // @ts-ignore - delete g.importScripts; - // @ts-ignore - const savedCrypto = g.crypto; - // @ts-ignore - delete g.crypto; - - // Assume that the code is run from the build/ directory. - const libFn = indirectRequire( - "../../../emscripten/taler-emscripten-lib.js", - ); - lib = libFn(libArgs); - - // @ts-ignore - g.importScripts = savedImportScripts; - // @ts-ignore - g.crypto = savedCrypto; - - if (!lib) { - throw Error("could not load taler-emscripten-lib.js"); - } - - if (!lib.ccall) { - throw Error( - "sanity check failed: taler-emscripten lib does not have 'ccall'", - ); - } - }); - } - private dispatchMessage(msg: any) { if (this.onmessage) { this.onmessage({ data: msg }); @@ -154,7 +84,7 @@ export class SynchronousCryptoWorker { } private async handleRequest(operation: string, id: number, args: string[]) { - let emsc = await this.getEmscriptenEnvironment(); + let emsc = await this.emscriptenLoader.getEmscriptenEnvironment(); const impl = new CryptoImplementation(emsc); diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts index 1e501cdc6..49cc608d9 100644 --- a/src/headless/taler-wallet-cli.ts +++ b/src/headless/taler-wallet-cli.ts @@ -26,7 +26,7 @@ import URI = require("urijs"); import querystring = require("querystring"); import { CheckPaymentResponse } from "../talerTypes"; -import { NodeCryptoWorkerFactory, SynchronousCryptoWorkerFactory } from "../crypto/cryptoApi"; +import { SynchronousCryptoWorkerFactory } from "../crypto/synchronousWorker"; const enableTracing = false; -- cgit v1.2.3