From 82a2437c0967871d6b942105c98c3382978cad29 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 6 Aug 2020 00:30:36 +0530 Subject: towards integration tests with fault injection --- .../src/crypto/workers/cryptoApi.ts | 12 +- .../src/crypto/workers/nodeThreadWorker.ts | 19 ++- .../taler-wallet-core/src/headless/NodeHttpLib.ts | 10 +- packages/taler-wallet-core/src/index.ts | 7 + .../taler-wallet-core/src/operations/exchanges.ts | 10 ++ packages/taler-wallet-core/src/operations/pay.ts | 2 +- .../taler-wallet-core/src/operations/withdraw.ts | 2 + .../taler-wallet-core/src/types/walletTypes.ts | 17 +++ packages/taler-wallet-core/src/util/http.ts | 7 + .../taler-wallet-core/src/util/talerconfig-test.ts | 124 +++++++++++++++++ packages/taler-wallet-core/src/util/talerconfig.ts | 151 ++++++++++++++++++++- packages/taler-wallet-core/src/util/timer.ts | 36 +++++ 12 files changed, 380 insertions(+), 17 deletions(-) create mode 100644 packages/taler-wallet-core/src/util/talerconfig-test.ts (limited to 'packages/taler-wallet-core/src') diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts index a272d5724..20d13a3f2 100644 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts +++ b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts @@ -1,17 +1,17 @@ /* - This file is part of TALER + This file is part of GNU Taler (C) 2016 GNUnet e.V. - TALER is free software; you can redistribute it and/or modify it under the + 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. - TALER is distributed in the hope that it will be useful, but WITHOUT ANY + 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 - TALER; see the file COPYING. If not, see + GNU Taler; see the file COPYING. If not, see */ /** @@ -46,6 +46,7 @@ import { import * as timer from "../../util/timer"; import { Logger } from "../../util/logging"; +import { walletCoreApi } from "../.."; const logger = new Logger("cryptoApi.ts"); @@ -182,7 +183,7 @@ export class CryptoApi { }; this.resetWorkerTimeout(ws); work.startTime = timer.performanceNow(); - setTimeout(() => worker.postMessage(msg), 0); + timer.after(0, () => worker.postMessage(msg)); } resetWorkerTimeout(ws: WorkerState): void { @@ -198,6 +199,7 @@ export class CryptoApi { } }; ws.terminationTimerHandle = timer.after(15 * 1000, destroy); + //ws.terminationTimerHandle.unref(); } handleWorkerError(ws: WorkerState, e: any): void { diff --git a/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts index 6c9dfc569..d4d858330 100644 --- a/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts +++ b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts @@ -21,6 +21,9 @@ import { CryptoWorkerFactory } from "./cryptoApi"; import { CryptoWorker } from "./cryptoWorker"; import os from "os"; import { CryptoImplementation } from "./cryptoImplementation"; +import { Logger } from "../../util/logging"; + +const logger = new Logger("nodeThreadWorker.ts"); const f = __filename; @@ -37,16 +40,22 @@ const workerCode = ` try { tw = require("${f}"); } catch (e) { - console.log("could not load from ${f}"); + console.warn("could not load from ${f}"); } if (!tw) { try { tw = require("taler-wallet-android"); } catch (e) { - console.log("could not load taler-wallet-android either"); + console.warn("could not load taler-wallet-android either"); throw e; } } + if (typeof tw.handleWorkerMessage !== "function") { + throw Error("module loaded for crypto worker lacks handleWorkerMessage"); + } + if (typeof tw.handleWorkerError !== "function") { + throw Error("module loaded for crypto worker lacks handleWorkerError"); + } parentPort.on("message", tw.handleWorkerMessage); parentPort.on("error", tw.handleWorkerError); `; @@ -138,6 +147,9 @@ class NodeThreadCryptoWorker implements CryptoWorker { constructor() { // eslint-disable-next-line @typescript-eslint/no-var-requires const worker_threads = require("worker_threads"); + + logger.trace("starting node crypto worker"); + this.nodeWorker = new worker_threads.Worker(workerCode, { eval: true }); this.nodeWorker.on("error", (err: Error) => { console.error("error in node worker:", err); @@ -145,6 +157,9 @@ class NodeThreadCryptoWorker implements CryptoWorker { this.onerror(err); } }); + this.nodeWorker.on("exit", (err) => { + logger.trace(`worker exited with code ${err}`); + }); this.nodeWorker.on("message", (v: any) => { if (this.onmessage) { this.onmessage(v); diff --git a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts index d109c3b7c..59730ab30 100644 --- a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts +++ b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts @@ -45,7 +45,7 @@ export class NodeHttpLib implements HttpRequestLibrary { } private async req( - method: "post" | "get", + method: "POST" | "GET", url: string, body: any, opt?: HttpRequestOptions, @@ -72,6 +72,7 @@ export class NodeHttpLib implements HttpRequestLibrary { { httpStatusCode: resp.status, requestUrl: url, + requestMethod: method, }, ), ); @@ -88,6 +89,7 @@ export class NodeHttpLib implements HttpRequestLibrary { { httpStatusCode: resp.status, requestUrl: url, + requestMethod: method, }, ), ); @@ -100,6 +102,7 @@ export class NodeHttpLib implements HttpRequestLibrary { { httpStatusCode: resp.status, requestUrl: url, + requestMethod: method, }, ), ); @@ -112,6 +115,7 @@ export class NodeHttpLib implements HttpRequestLibrary { } return { requestUrl: url, + requestMethod: method, headers, status: resp.status, text: async () => resp.data, @@ -120,7 +124,7 @@ export class NodeHttpLib implements HttpRequestLibrary { } async get(url: string, opt?: HttpRequestOptions): Promise { - return this.req("get", url, undefined, opt); + return this.req("GET", url, undefined, opt); } async postJson( @@ -128,6 +132,6 @@ export class NodeHttpLib implements HttpRequestLibrary { body: any, opt?: HttpRequestOptions, ): Promise { - return this.req("post", url, body, opt); + return this.req("POST", url, body, opt); } } diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts index e70fc44f6..5c4961bd7 100644 --- a/packages/taler-wallet-core/src/index.ts +++ b/packages/taler-wallet-core/src/index.ts @@ -73,3 +73,10 @@ export * as i18n from "./i18n"; export * as nodeThreadWorker from "./crypto/workers/nodeThreadWorker"; export * as walletNotifications from "./types/notifications"; + +export { Configuration } from "./util/talerconfig"; + +export { + handleWorkerMessage, + handleWorkerError, +} from "./crypto/workers/nodeThreadWorker"; diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index ee49fddb5..8967173ca 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -112,6 +112,8 @@ async function updateExchangeWithKeys( return; } + logger.info("updating exchange /keys info"); + const keysUrl = new URL("keys", baseUrl); keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION); @@ -121,6 +123,8 @@ async function updateExchangeWithKeys( codecForExchangeKeysJson(), ); + logger.info("received /keys response"); + if (exchangeKeysJson.denoms.length === 0) { const opErr = makeErrorDetails( TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT, @@ -152,12 +156,16 @@ async function updateExchangeWithKeys( const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value) .currency; + logger.trace("processing denominations"); + const newDenominations = await Promise.all( exchangeKeysJson.denoms.map((d) => denominationRecordFromKeys(ws, baseUrl, d), ), ); + logger.trace("done with processing denominations"); + const lastUpdateTimestamp = getTimestampNow(); const recoupGroupId: string | undefined = undefined; @@ -241,6 +249,8 @@ async function updateExchangeWithKeys( console.log("error while recouping coins:", e); }); } + + logger.trace("done updating exchange /keys"); } async function updateExchangeFinalize( diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index f23e326f8..0fa9e0a61 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -781,7 +781,7 @@ export async function submitPay( } const sessionId = purchase.lastSessionId; - console.log("paying with session ID", sessionId); + logger.trace("paying with session ID", sessionId); const payUrl = new URL( `orders/${purchase.contractData.orderId}/pay`, diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 3b0aa0095..9719772a7 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -712,7 +712,9 @@ export async function getWithdrawalDetailsForUri( ws: InternalWalletState, talerWithdrawUri: string, ): Promise { + logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`); const info = await getBankWithdrawalInfo(ws, talerWithdrawUri); + logger.trace(`got bank info`); if (info.suggestedExchange) { // FIXME: right now the exchange gets permanently added, // we might want to only temporarily add it. diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts b/packages/taler-wallet-core/src/types/walletTypes.ts index 04f50f29a..83275a0cc 100644 --- a/packages/taler-wallet-core/src/types/walletTypes.ts +++ b/packages/taler-wallet-core/src/types/walletTypes.ts @@ -40,8 +40,11 @@ import { codecForString, makeCodecOptional, Codec, + makeCodecForList, + codecForBoolean, } from "../util/codec"; import { AmountString } from "./talerTypes"; +import { codec } from ".."; /** * Response for the create reserve request to the wallet. @@ -164,6 +167,20 @@ export interface BalancesResponse { balances: Balance[]; } +export const codecForBalance = (): Codec => + makeCodecForObject() + .property("available", codecForString) + .property("hasPendingTransactions", codecForBoolean) + .property("pendingIncoming", codecForString) + .property("pendingOutgoing", codecForString) + .property("requiresUserInput", codecForBoolean) + .build("Balance"); + +export const codecForBalancesResponse = (): Codec => + makeCodecForObject() + .property("balances", makeCodecForList(codecForBalance())) + .build("BalancesResponse"); + /** * For terseness. */ diff --git a/packages/taler-wallet-core/src/util/http.ts b/packages/taler-wallet-core/src/util/http.ts index ad9f0293c..72de2ed1d 100644 --- a/packages/taler-wallet-core/src/util/http.ts +++ b/packages/taler-wallet-core/src/util/http.ts @@ -34,6 +34,7 @@ const logger = new Logger("http.ts"); */ export interface HttpResponse { requestUrl: string; + requestMethod: string; status: number; headers: Headers; json(): Promise; @@ -118,6 +119,8 @@ export async function readSuccessResponseJsonOrErrorCode( "Error response did not contain error code", { requestUrl: httpResponse.requestUrl, + requestMethod: httpResponse.requestMethod, + httpStatusCode: httpResponse.status, }, ), ); @@ -188,7 +191,9 @@ export async function readSuccessResponseTextOrErrorCode( TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, "Error response did not contain error code", { + httpStatusCode: httpResponse.status, requestUrl: httpResponse.requestUrl, + requestMethod: httpResponse.requestMethod, }, ), ); @@ -217,7 +222,9 @@ export async function checkSuccessResponseOrThrow( TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, "Error response did not contain error code", { + httpStatusCode: httpResponse.status, requestUrl: httpResponse.requestUrl, + requestMethod: httpResponse.requestMethod, }, ), ); diff --git a/packages/taler-wallet-core/src/util/talerconfig-test.ts b/packages/taler-wallet-core/src/util/talerconfig-test.ts new file mode 100644 index 000000000..71359fd38 --- /dev/null +++ b/packages/taler-wallet-core/src/util/talerconfig-test.ts @@ -0,0 +1,124 @@ +/* + This file is part of GNU Taler + (C) 2020 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 + */ + +/** + * Imports + */ +import test from "ava"; +import { pathsub, Configuration } from "./talerconfig"; + +test("pathsub", (t) => { + t.assert("foo" === pathsub("foo", () => undefined)); + + t.assert("fo${bla}o" === pathsub("fo${bla}o", () => undefined)); + + const d: Record = { + w: "world", + f: "foo", + "1foo": "x", + "foo_bar": "quux", + }; + + t.is( + pathsub("hello ${w}!", (v) => d[v]), + "hello world!", + ); + + t.is( + pathsub("hello ${w} ${w}!", (v) => d[v]), + "hello world world!", + ); + + t.is( + pathsub("hello ${x:-blabla}!", (v) => d[v]), + "hello blabla!", + ); + + // No braces + t.is( + pathsub("hello $w!", (v) => d[v]), + "hello world!", + ); + t.is( + pathsub("hello $foo!", (v) => d[v]), + "hello $foo!", + ); + t.is( + pathsub("hello $1foo!", (v) => d[v]), + "hello $1foo!", + ); + t.is( + pathsub("hello $$ world!", (v) => d[v]), + "hello $$ world!", + ); + t.is( + pathsub("hello $$ world!", (v) => d[v]), + "hello $$ world!", + ); + + t.is( + pathsub("hello $foo_bar!", (v) => d[v]), + "hello quux!", + ); + + // Recursive lookup in default + t.is( + pathsub("hello ${x:-${w}}!", (v) => d[v]), + "hello world!", + ); + + // No variables in variable name part + t.is( + pathsub("hello ${${w}:-x}!", (v) => d[v]), + "hello ${${w}:-x}!", + ); + + // Missing closing brace + t.is( + pathsub("hello ${w!", (v) => d[v]), + "hello ${w!", + ); +}); + +test("path expansion", (t) => { + const config = new Configuration(); + config.setString("paths", "taler_home", "foo/bar"); + config.setString( + "paths", + "taler_data_home", + "$TALER_HOME/.local/share/taler/", + ); + config.setString( + "exchange", + "master_priv_file", + "${TALER_DATA_HOME}/exchange/offline-keys/master.priv", + ); + t.is( + config.getPath("exchange", "MaStER_priv_file").required(), + "foo/bar/.local/share/taler//exchange/offline-keys/master.priv", + ); +}); + +test("recursive path resolution", (t) => { + console.log("recursive test"); + const config = new Configuration(); + config.setString("paths", "a", "x${b}"); + config.setString("paths", "b", "y${a}"); + config.setString("foo", "x", "z${a}"); + t.throws(() => { + config.getPath("foo", "a").required(); + }); +}); diff --git a/packages/taler-wallet-core/src/util/talerconfig.ts b/packages/taler-wallet-core/src/util/talerconfig.ts index ec08c352f..61bb6d206 100644 --- a/packages/taler-wallet-core/src/util/talerconfig.ts +++ b/packages/taler-wallet-core/src/util/talerconfig.ts @@ -25,6 +25,8 @@ */ import { AmountJson } from "./amounts"; import * as Amounts from "./amounts"; +import fs from "fs"; +import { acceptExchangeTermsOfService } from "../operations/exchanges"; export class ConfigError extends Error { constructor(message: string) { @@ -56,6 +58,89 @@ export class ConfigValue { } } +/** + * Shell-style path substitution. + * + * Supported patterns: + * "$x" (look up "x") + * "${x}" (look up "x") + * "${x:-y}" (look up "x", fall back to expanded y) + */ +export function pathsub( + x: string, + lookup: (s: string, depth: number) => string | undefined, + depth = 0, +): string { + if (depth >= 10) { + throw Error("recursion in path substitution"); + } + let s = x; + let l = 0; + while (l < s.length) { + if (s[l] === "$") { + if (s[l + 1] === "{") { + let depth = 1; + const start = l; + let p = start + 2; + let insideNamePart = true; + let hasDefault = false; + for (; p < s.length; p++) { + if (s[p] == "}") { + insideNamePart = false; + depth--; + } else if (s[p] === "$" && s[p + 1] === "{") { + insideNamePart = false; + depth++; + } + if (insideNamePart && s[p] === ":" && s[p + 1] === "-") { + hasDefault = true; + } + if (depth == 0) { + break; + } + } + if (depth == 0) { + const inner = s.slice(start + 2, p); + let varname: string; + let defaultValue: string | undefined; + if (hasDefault) { + [varname, defaultValue] = inner.split(":-", 2); + } else { + varname = inner; + defaultValue = undefined; + } + + const r = lookup(inner, depth + 1); + if (r !== undefined) { + s = s.substr(0, start) + r + s.substr(p + 1); + l = start + r.length; + continue; + } else if (defaultValue !== undefined) { + const resolvedDefault = pathsub(defaultValue, lookup, depth + 1); + s = s.substr(0, start) + resolvedDefault + s.substr(p + 1); + l = start + resolvedDefault.length; + continue; + } + } + l = p; + continue; + } else { + const m = /^[a-zA-Z-_][a-zA-Z0-9-_]*/.exec(s.substring(l + 1)); + if (m && m[0]) { + const r = lookup(m[0], depth + 1); + if (r !== undefined) { + s = s.substr(0, l) + r + s.substr(l + 1 + m[0].length); + l = l + r.length; + continue; + } + } + } + } + l++; + } + return s; +} + export class Configuration { private sectionMap: SectionMap = {}; @@ -69,7 +154,6 @@ export class Configuration { const lines = s.split("\n"); for (const line of lines) { - console.log("parsing line", JSON.stringify(line)); if (reEmptyLine.test(line)) { continue; } @@ -79,15 +163,15 @@ export class Configuration { const secMatch = line.match(reSection); if (secMatch) { currentSection = secMatch[1]; - console.log("setting section to", currentSection); continue; } if (currentSection === undefined) { throw Error("invalid configuration, expected section header"); } + currentSection = currentSection.toUpperCase(); const paramMatch = line.match(reParam); if (paramMatch) { - const optName = paramMatch[1]; + const optName = paramMatch[1].toUpperCase(); let val = paramMatch[2]; if (val.startsWith('"') && val.endsWith('"')) { val = val.slice(1, val.length - 1); @@ -102,13 +186,44 @@ export class Configuration { "invalid configuration, expected section header or option assignment", ); } + } - console.log("parsed config", JSON.stringify(this.sectionMap, undefined, 2)); + setString(section: string, option: string, value: string): void { + const secNorm = section.toUpperCase(); + const sec = this.sectionMap[secNorm] ?? (this.sectionMap[secNorm] = {}); + sec[option.toUpperCase()] = value; } getString(section: string, option: string): ConfigValue { - const val = (this.sectionMap[section] ?? {})[option]; - return new ConfigValue(section, option, val, (x) => x); + const secNorm = section.toUpperCase(); + const optNorm = option.toUpperCase(); + const val = (this.sectionMap[section] ?? {})[optNorm]; + return new ConfigValue(secNorm, optNorm, val, (x) => x); + } + + getPath(section: string, option: string): ConfigValue { + const secNorm = section.toUpperCase(); + const optNorm = option.toUpperCase(); + const val = (this.sectionMap[secNorm] ?? {})[optNorm]; + return new ConfigValue(secNorm, optNorm, val, (x) => + pathsub(x, (v, d) => this.lookupVariable(v, d + 1)), + ); + } + + lookupVariable(x: string, depth: number = 0): string | undefined { + console.log("looking up", x); + // We loop up options in PATHS in upper case, as option names + // are case insensitive + const val = (this.sectionMap["PATHS"] ?? {})[x.toUpperCase()]; + if (val !== undefined) { + return pathsub(val, (v, d) => this.lookupVariable(v, d), depth); + } + // Environment variables can be case sensitive, respect that. + const envVal = process.env[x]; + if (envVal !== undefined) { + return envVal; + } + return; } getAmount(section: string, option: string): ConfigValue { @@ -117,4 +232,28 @@ export class Configuration { Amounts.parseOrThrow(x), ); } + + static load(filename: string): Configuration { + const s = fs.readFileSync(filename, "utf-8"); + const cfg = new Configuration(); + cfg.loadFromString(s); + return cfg; + } + + write(filename: string): void { + let s = ""; + for (const sectionName of Object.keys(this.sectionMap)) { + s += `[${sectionName}]\n`; + for (const optionName of Object.keys( + this.sectionMap[sectionName] ?? {}, + )) { + const val = this.sectionMap[sectionName][optionName]; + if (val !== undefined) { + s += `${optionName} = ${val}\n`; + } + } + s += "\n"; + } + fs.writeFileSync(filename, s); + } } diff --git a/packages/taler-wallet-core/src/util/timer.ts b/packages/taler-wallet-core/src/util/timer.ts index 8eab1399c..d652fdcda 100644 --- a/packages/taler-wallet-core/src/util/timer.ts +++ b/packages/taler-wallet-core/src/util/timer.ts @@ -34,6 +34,12 @@ const logger = new Logger("timer.ts"); */ export interface TimerHandle { clear(): void; + + /** + * Make sure the event loop exits when the timer is the + * only event left. Has no effect in the browser. + */ + unref(): void; } class IntervalHandle { @@ -42,6 +48,16 @@ class IntervalHandle { clear(): void { clearInterval(this.h); } + + /** + * Make sure the event loop exits when the timer is the + * only event left. Has no effect in the browser. + */ + unref(): void { + if (typeof this.h === "object") { + this.h.unref(); + } + } } class TimeoutHandle { @@ -50,6 +66,16 @@ class TimeoutHandle { clear(): void { clearTimeout(this.h); } + + /** + * Make sure the event loop exits when the timer is the + * only event left. Has no effect in the browser. + */ + unref(): void { + if (typeof this.h === "object") { + this.h.unref(); + } + } } /** @@ -92,6 +118,10 @@ const nullTimerHandle = { // do nothing return; }, + unref() { + // do nothing + return; + } }; /** @@ -141,6 +171,9 @@ export class TimerGroup { h.clear(); delete tm[myId]; }, + unref() { + h.unref(); + } }; } @@ -160,6 +193,9 @@ export class TimerGroup { h.clear(); delete tm[myId]; }, + unref() { + h.unref(); + } }; } } -- cgit v1.2.3