diff options
Diffstat (limited to 'packages/taler-wallet-cli/src/lint.ts')
-rw-r--r-- | packages/taler-wallet-cli/src/lint.ts | 534 |
1 files changed, 0 insertions, 534 deletions
diff --git a/packages/taler-wallet-cli/src/lint.ts b/packages/taler-wallet-cli/src/lint.ts deleted file mode 100644 index 49fb9dc86..000000000 --- a/packages/taler-wallet-cli/src/lint.ts +++ /dev/null @@ -1,534 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 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/> - */ - -/** - * The deployment linter implements checks for a deployment - * of the GNU Taler exchange. It is meant to help sysadmins - * when setting up an exchange. - * - * The linter does checks in the configuration and uses - * various tools of the exchange in test mode (-t). - * - * To be able to run the tools as the right user, the linter should be - * run as root. - * - * @author Florian Dold <dold@taler.net> - */ - -/** - * Imports. - */ -import { - codecForExchangeKeysJson, - codecForKeysManagementResponse, - Configuration, - decodeCrock, -} from "@gnu-taler/taler-util"; -import { - NodeHttpLib, - readSuccessResponseJsonOrThrow, -} from "@gnu-taler/taler-wallet-core"; -import { URL } from "url"; -import { spawn } from "child_process"; -import { delayMs } from "./harness/harness.js"; - -interface BasicConf { - mainCurrency: string; -} - -interface PubkeyConf { - masterPublicKey: string; -} - -const httpLib = new NodeHttpLib(); - -interface ShellResult { - stdout: string; - stderr: string; - status: number; -} - -interface LintContext { - /** - * Be more verbose. - */ - verbose: boolean; - - /** - * Always continue even after errors. - */ - cont: boolean; - - cfg: Configuration; - - numErr: number; -} - -/** - * Run a shell command, return stdout. - */ -export async function sh( - context: LintContext, - command: string, - env: { [index: string]: string | undefined } = process.env, -): Promise<ShellResult> { - if (context.verbose) { - console.log("executing command:", command); - } - return new Promise((resolve, reject) => { - const stdoutChunks: Buffer[] = []; - const stderrChunks: Buffer[] = []; - const proc = spawn(command, { - stdio: ["inherit", "pipe", "pipe"], - shell: true, - env: env, - }); - proc.stdout.on("data", (x) => { - if (x instanceof Buffer) { - stdoutChunks.push(x); - } else { - throw Error("unexpected data chunk type"); - } - }); - proc.stderr.on("data", (x) => { - if (x instanceof Buffer) { - stderrChunks.push(x); - } else { - throw Error("unexpected data chunk type"); - } - }); - proc.on("exit", (code, signal) => { - if (code != 0 && context.verbose) { - console.log(`child process exited (${code} / ${signal})`); - } - const bOut = Buffer.concat(stdoutChunks).toString("utf-8"); - const bErr = Buffer.concat(stderrChunks).toString("utf-8"); - resolve({ - status: code ?? -1, - stderr: bErr, - stdout: bOut, - }); - }); - proc.on("error", () => { - reject(Error("Child process had error")); - }); - }); -} - -function checkBasicConf(context: LintContext): BasicConf { - const cfg = context.cfg; - const currencyEntry = cfg.getString("taler", "currency"); - let mainCurrency: string | undefined; - - if (!currencyEntry.value) { - context.numErr++; - console.log("error: currency not defined in section TALER option CURRENCY"); - console.log("Aborting further checks."); - process.exit(1); - } else { - mainCurrency = currencyEntry.value.toUpperCase(); - } - - if (mainCurrency === "KUDOS") { - console.log( - "warning: section TALER option CURRENCY contains toy currency value KUDOS", - ); - } - - const roundUnit = cfg.getAmount("taler", "currency_round_unit"); - const ru = roundUnit.required(); - if (ru.currency.toLowerCase() != mainCurrency.toLowerCase()) { - context.numErr++; - console.log( - "error: [TALER]/CURRENCY_ROUND_UNIT: currency does not match main currency", - ); - } - return { mainCurrency }; -} - -function checkCoinConfig(context: LintContext, basic: BasicConf): void { - const cfg = context.cfg; - const coinPrefix1 = "COIN_"; - const coinPrefix2 = "COIN-"; - let numCoins = 0; - - for (const secName of cfg.getSectionNames()) { - if (!(secName.startsWith(coinPrefix1) || secName.startsWith(coinPrefix2))) { - continue; - } - numCoins++; - - // FIXME: check that section is well-formed - } - - if (numCoins == 0) { - context.numErr++; - console.log( - "error: no coin denomination configured, please configure [coin-*] sections", - ); - } -} - -async function checkWireConfig(context: LintContext): Promise<void> { - const cfg = context.cfg; - const accountPrefix = "EXCHANGE-ACCOUNT-"; - const accountCredentialsPrefix = "EXCHANGE-ACCOUNTCREDENTIALS-"; - - let accounts = new Set<string>(); - let credentials = new Set<string>(); - - for (const secName of cfg.getSectionNames()) { - if (secName.startsWith(accountPrefix)) { - accounts.add(secName.slice(accountPrefix.length)); - // FIXME: check settings - } - - if (secName.startsWith(accountCredentialsPrefix)) { - credentials.add(secName.slice(accountCredentialsPrefix.length)); - // FIXME: check settings - } - } - - if (accounts.size === 0) { - context.numErr++; - console.log( - "error: No accounts configured (no sections EXCHANGE-ACCOUNT-*).", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - for (const acc of accounts) { - if (!credentials.has(acc)) { - console.log( - `warning: no credentials configured for exchange-account-${acc}`, - ); - } - } - - for (const acc of accounts) { - // test credit history - { - const res = await sh( - context, - "su -l --shell /bin/sh " + - `-c 'taler-exchange-wire-gateway-client -s exchange-accountcredentials-${acc} --credit-history' ` + - "taler-exchange-wire", - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log( - "error: Could not run taler-exchange-wire-gateway-client. Please review logs above.", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } - } - - // TWG client - { - const res = await sh( - context, - `su -l --shell /bin/sh -c 'taler-exchange-wirewatch -t' taler-exchange-wire`, - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run wirewatch. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } - - // Wirewatch - { - const res = await sh( - context, - `su -l --shell /bin/sh -c 'taler-exchange-wirewatch -t' taler-exchange-wire`, - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run wirewatch. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } - - // Closer - { - const res = await sh( - context, - `su -l --shell /bin/sh -c 'taler-exchange-closer -t' taler-exchange-closer`, - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run closer. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } -} - -async function checkAggregatorConfig(context: LintContext) { - const res = await sh( - context, - "su -l --shell /bin/sh -c 'taler-exchange-aggregator -t' taler-exchange-aggregator", - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run aggregator. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } -} - -async function checkCloserConfig(context: LintContext) { - const res = await sh( - context, - `su -l --shell /bin/sh -c 'taler-exchange-closer -t' taler-exchange-closer`, - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run closer. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } -} - -function checkMasterPublicKeyConfig(context: LintContext): PubkeyConf { - const cfg = context.cfg; - const pub = cfg.getString("exchange", "master_public_key"); - - const pubDecoded = decodeCrock(pub.required()); - - if (pubDecoded.length != 32) { - context.numErr++; - console.log("error: invalid master public key"); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - return { - masterPublicKey: pub.required(), - }; -} - -export async function checkExchangeHttpd( - context: LintContext, - pubConf: PubkeyConf, -): Promise<void> { - const cfg = context.cfg; - const baseUrlEntry = cfg.getString("exchange", "base_url"); - - if (!baseUrlEntry.isDefined) { - context.numErr++; - console.log( - "error: configuration needs to specify section EXCHANGE option BASE_URL", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - const baseUrl = baseUrlEntry.required(); - - if (!baseUrl.startsWith("http")) { - context.numErr++; - console.log( - "error: section EXCHANGE option BASE_URL needs to be an http or https URL", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - if (!baseUrl.endsWith("/")) { - context.numErr++; - console.log( - "error: section EXCHANGE option BASE_URL needs to end with a slash", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - if (!baseUrl.startsWith("https://")) { - console.log( - "warning: section EXCHANGE option BASE_URL: it is recommended to serve the exchange via HTTPS", - ); - } - - { - const mgmtUrl = new URL("management/keys", baseUrl); - const resp = await httpLib.get(mgmtUrl.href); - - const futureKeys = await readSuccessResponseJsonOrThrow( - resp, - codecForKeysManagementResponse(), - ); - - if (futureKeys.future_denoms.length > 0) { - console.log( - `warning: exchange has denomination keys that need to be signed by the offline signing procedure`, - ); - } - - if (futureKeys.future_signkeys.length > 0) { - console.log( - `warning: exchange has signing keys that need to be signed by the offline signing procedure`, - ); - } - } - - // Check if we can use /keys already - { - const keysUrl = new URL("keys", baseUrl); - - const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]); - - if (!resp) { - context.numErr++; - console.log( - "error: request to /keys timed out. " + - "Make sure to sign and upload denomination and signing keys " + - "with taler-exchange-offline.", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } else { - const keys = await readSuccessResponseJsonOrThrow( - resp, - codecForExchangeKeysJson(), - ); - - if (keys.master_public_key !== pubConf.masterPublicKey) { - context.numErr++; - console.log( - "error: master public key of exchange does not match public key of live exchange", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } - } - - // Check /wire - { - const keysUrl = new URL("wire", baseUrl); - - const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]); - - if (!resp) { - context.numErr++; - console.log( - "error: request to /wire timed out. " + - "Make sure to sign and upload accounts and wire fees " + - "using the taler-exchange-offline tool.", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } else { - if (resp.status !== 200) { - console.log( - "error: Can't access exchange /wire. Please check " + - "the logs of taler-exchange-httpd for further information.", - ); - } - } - } -} - -/** - * Do some basic checks in the configuration of a Taler deployment. - */ -export async function lintExchangeDeployment( - verbose: boolean, - cont: boolean, -): Promise<void> { - if (process.getuid!() != 0) { - console.log( - "warning: the exchange deployment linter is designed to be run as root", - ); - } - - const cfg = Configuration.load(); - - const context: LintContext = { - cont, - verbose, - cfg, - numErr: 0, - }; - - const basic = checkBasicConf(context); - - checkCoinConfig(context, basic); - - await checkWireConfig(context); - - await checkAggregatorConfig(context); - - await checkCloserConfig(context); - - const pubConf = checkMasterPublicKeyConfig(context); - - await checkExchangeHttpd(context, pubConf); - - if (context.numErr == 0) { - console.log("Linting completed without errors."); - process.exit(0); - } else { - console.log(`Linting completed with ${context.numErr} errors.`); - process.exit(1); - } -} |