diff options
Diffstat (limited to 'packages/taler-wallet-core/src/headless')
3 files changed, 269 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/headless/NodeHttpLib.d.ts.map b/packages/taler-wallet-core/src/headless/NodeHttpLib.d.ts.map new file mode 100644 index 000000000..06ba7a3e1 --- /dev/null +++ b/packages/taler-wallet-core/src/headless/NodeHttpLib.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"NodeHttpLib.d.ts","sourceRoot":"","sources":["NodeHttpLib.ts"],"names":[],"mappings":"AAkBA;;GAEG;AACH,OAAO,EAEL,kBAAkB,EAClB,kBAAkB,EAClB,YAAY,EACb,MAAM,cAAc,CAAC;AAMtB;;GAEG;AACH,qBAAa,WAAY,YAAW,kBAAkB;IACpD,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,iBAAiB,CAAQ;IAEjC;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;YAIvB,GAAG;IA2EX,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC;IAIjE,QAAQ,CACZ,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,GAAG,EACT,GAAG,CAAC,EAAE,kBAAkB,GACvB,OAAO,CAAC,YAAY,CAAC;CAGzB"}
\ No newline at end of file diff --git a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts new file mode 100644 index 000000000..d109c3b7c --- /dev/null +++ b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts @@ -0,0 +1,133 @@ +/* + This file is part of GNU Taler + (C) 2019 Taler Systems S.A. + + GNU 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. + + GNU 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 + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + + SPDX-License-Identifier: AGPL3.0-or-later +*/ + +/** + * Imports. + */ +import { + Headers, + HttpRequestLibrary, + HttpRequestOptions, + HttpResponse, +} from "../util/http"; +import { RequestThrottler } from "../util/RequestThrottler"; +import Axios from "axios"; +import { OperationFailedError, makeErrorDetails } from "../operations/errors"; +import { TalerErrorCode } from "../TalerErrorCode"; + +/** + * Implementation of the HTTP request library interface for node. + */ +export class NodeHttpLib implements HttpRequestLibrary { + private throttle = new RequestThrottler(); + private throttlingEnabled = true; + + /** + * Set whether requests should be throttled. + */ + setThrottling(enabled: boolean): void { + this.throttlingEnabled = enabled; + } + + private async req( + method: "post" | "get", + url: string, + body: any, + opt?: HttpRequestOptions, + ): Promise<HttpResponse> { + if (this.throttlingEnabled && this.throttle.applyThrottle(url)) { + throw Error("request throttled"); + } + const resp = await Axios({ + method, + url: url, + responseType: "text", + headers: opt?.headers, + validateStatus: () => true, + transformResponse: (x) => x, + data: body, + }); + + const respText = resp.data; + if (typeof respText !== "string") { + throw new OperationFailedError( + makeErrorDetails( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + "unexpected response type", + { + httpStatusCode: resp.status, + requestUrl: url, + }, + ), + ); + } + const makeJson = async (): Promise<any> => { + let responseJson; + try { + responseJson = JSON.parse(respText); + } catch (e) { + throw new OperationFailedError( + makeErrorDetails( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + "invalid JSON", + { + httpStatusCode: resp.status, + requestUrl: url, + }, + ), + ); + } + if (responseJson === null || typeof responseJson !== "object") { + throw new OperationFailedError( + makeErrorDetails( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + "invalid JSON", + { + httpStatusCode: resp.status, + requestUrl: url, + }, + ), + ); + } + return responseJson; + }; + const headers = new Headers(); + for (const hn of Object.keys(resp.headers)) { + headers.set(hn, resp.headers[hn]); + } + return { + requestUrl: url, + headers, + status: resp.status, + text: async () => resp.data, + json: makeJson, + }; + } + + async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> { + return this.req("get", url, undefined, opt); + } + + async postJson( + url: string, + body: any, + opt?: HttpRequestOptions, + ): Promise<HttpResponse> { + return this.req("post", url, body, opt); + } +} diff --git a/packages/taler-wallet-core/src/headless/helpers.ts b/packages/taler-wallet-core/src/headless/helpers.ts new file mode 100644 index 000000000..953493299 --- /dev/null +++ b/packages/taler-wallet-core/src/headless/helpers.ts @@ -0,0 +1,135 @@ +/* + This file is part of GNU Taler + (C) 2019 Taler Systems S.A. + + GNU 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. + + GNU 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 + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Helpers to create headless wallets. + * @author Florian Dold <dold@taler.net> + */ + +/** + * Imports. + */ +import { Wallet } from "../wallet"; +import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge"; +import { openTalerDatabase } from "../db"; +import { HttpRequestLibrary } from "../util/http"; +import fs from "fs"; +import { NodeThreadCryptoWorkerFactory } from "../crypto/workers/nodeThreadWorker"; +import { WalletNotification } from "../types/notifications"; +import { Database } from "../util/query"; +import { NodeHttpLib } from "./NodeHttpLib"; +import { Logger } from "../util/logging"; +import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker"; +import type { IDBFactory } from "idb-bridge/lib/idbtypes"; + +const logger = new Logger("headless/helpers.ts"); + +export interface DefaultNodeWalletArgs { + /** + * Location of the wallet database. + * + * If not specified, the wallet starts out with an empty database and + * the wallet database is stored only in memory. + */ + persistentStoragePath?: string; + + /** + * Handler for asynchronous notifications from the wallet. + */ + notifyHandler?: (n: WalletNotification) => void; + + /** + * If specified, use this as HTTP request library instead + * of the default one. + */ + httpLib?: HttpRequestLibrary; +} + +/** + * Get a wallet instance with default settings for node. + */ +export async function getDefaultNodeWallet( + args: DefaultNodeWalletArgs = {}, +): Promise<Wallet> { + BridgeIDBFactory.enableTracing = false; + const myBackend = new MemoryBackend(); + myBackend.enableTracing = false; + + const storagePath = args.persistentStoragePath; + if (storagePath) { + try { + const dbContentStr: string = fs.readFileSync(storagePath, { + encoding: "utf-8", + }); + const dbContent = JSON.parse(dbContentStr); + myBackend.importDump(dbContent); + } catch (e) { + logger.warn("could not read wallet file"); + } + + myBackend.afterCommitCallback = async () => { + // Allow caller to stop persisting the wallet. + if (args.persistentStoragePath === undefined) { + return; + } + const dbContent = myBackend.exportDump(); + fs.writeFileSync(storagePath, JSON.stringify(dbContent, undefined, 2), { + encoding: "utf-8", + }); + }; + } + + BridgeIDBFactory.enableTracing = false; + + const myBridgeIdbFactory = new BridgeIDBFactory(myBackend); + const myIdbFactory: IDBFactory = (myBridgeIdbFactory as any) as IDBFactory; + + let myHttpLib; + if (args.httpLib) { + myHttpLib = args.httpLib; + } else { + myHttpLib = new NodeHttpLib(); + } + + const myVersionChange = (): Promise<void> => { + console.error("version change requested, should not happen"); + throw Error(); + }; + + shimIndexedDB(myBridgeIdbFactory); + + const myDb = await openTalerDatabase(myIdbFactory, myVersionChange); + + let workerFactory; + try { + // Try if we have worker threads available, fails in older node versions. + require("worker_threads"); + workerFactory = new NodeThreadCryptoWorkerFactory(); + } catch (e) { + console.log( + "worker threads not available, falling back to synchronous workers", + ); + workerFactory = new SynchronousCryptoWorkerFactory(); + } + + const dbWrap = new Database(myDb); + + const w = new Wallet(dbWrap, myHttpLib, workerFactory); + if (args.notifyHandler) { + w.addNotificationListener(args.notifyHandler); + } + return w; +} |