wallet-core/packages/taler-wallet-cli/src/lint.ts

495 lines
12 KiB
TypeScript
Raw Normal View History

2021-08-02 16:23:17 +02:00
/*
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/>
*/
2021-08-04 22:43:09 +02:00
/**
* 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>
*/
2021-08-02 16:23:17 +02:00
/**
* Imports.
*/
2021-08-04 17:14:52 +02:00
import {
codecForExchangeKeysJson,
codecForKeysManagementResponse,
Configuration,
} from "@gnu-taler/taler-util";
import {
decodeCrock,
NodeHttpLib,
readSuccessResponseJsonOrThrow,
} from "@gnu-taler/taler-wallet-core";
import { URL } from "url";
import * as fs from "fs";
import * as path from "path";
import { ChildProcess, spawn } from "child_process";
2021-08-04 21:58:21 +02:00
import { delayMs } from "./integrationtests/harness.js";
2021-08-04 17:14:52 +02:00
interface BasicConf {
mainCurrency: string;
}
interface PubkeyConf {
masterPublicKey: string;
}
const httpLib = new NodeHttpLib();
interface ShellResult {
stdout: string;
stderr: string;
status: number;
}
2021-08-02 16:23:17 +02:00
2021-08-04 22:24:06 +02:00
interface LintContext {
/**
* Be more verbose.
*/
verbose: boolean;
/**
* Always continue even after errors.
*/
cont: boolean;
cfg: Configuration;
}
2021-08-02 16:23:17 +02:00
/**
2021-08-04 17:14:52 +02:00
* Run a shell command, return stdout.
2021-08-02 16:23:17 +02:00
*/
2021-08-04 17:14:52 +02:00
export async function sh(
2021-08-04 22:24:06 +02:00
context: LintContext,
2021-08-04 17:14:52 +02:00
command: string,
env: { [index: string]: string | undefined } = process.env,
): Promise<ShellResult> {
2021-08-04 22:24:06 +02:00
if (context.verbose) {
console.log("executing command:", command);
}
2021-08-04 17:14:52 +02:00
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) => {
2021-08-04 22:32:23 +02:00
if (code != 0 && context.verbose) {
console.log(`child process exited (${code} / ${signal})`);
}
2021-08-04 17:14:52 +02:00
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"));
});
});
}
2021-08-04 22:24:06 +02:00
function checkBasicConf(context: LintContext): BasicConf {
const cfg = context.cfg;
2021-08-02 16:23:17 +02:00
const currencyEntry = cfg.getString("taler", "currency");
let mainCurrency: string | undefined;
if (!currencyEntry.value) {
console.log("error: currency not defined in section TALER option CURRENCY");
2021-08-04 22:24:06 +02:00
console.log("Aborting further checks.");
2021-08-04 17:14:52 +02:00
process.exit(1);
2021-08-02 16:23:17 +02:00
} else {
mainCurrency = currencyEntry.value.toUpperCase();
}
if (mainCurrency === "KUDOS") {
console.log(
"warning: section TALER option CURRENCY contains toy currency value KUDOS",
);
}
2021-08-04 17:14:52 +02:00
const roundUnit = cfg.getAmount("taler", "currency_round_unit");
if (!roundUnit.isDefined) {
console.log(
"error: configuration incomplete, section TALER option CURRENCY_ROUND_UNIT missing",
);
}
return { mainCurrency };
}
2021-08-04 22:24:06 +02:00
function checkCoinConfig(context: LintContext, basic: BasicConf): void {
const cfg = context.cfg;
2021-08-04 18:08:24 +02:00
const coinPrefix1 = "COIN_";
const coinPrefix2 = "COIN-";
2021-08-04 17:14:52 +02:00
let numCoins = 0;
for (const secName of cfg.getSectionNames()) {
2021-08-04 22:35:03 +02:00
if (!secName.startsWith(coinPrefix1) || !secName.startsWith(coinPrefix2)) {
2021-08-04 17:14:52 +02:00
continue;
}
numCoins++;
// FIXME: check that section is well-formed
}
2021-08-04 18:14:37 +02:00
if (numCoins == 0) {
console.log(
"error: no coin denomination configured, please configure [coin-*] sections",
);
}
2021-08-04 17:14:52 +02:00
}
2021-08-04 22:24:06 +02:00
async function checkWireConfig(context: LintContext): Promise<void> {
const cfg = context.cfg;
2021-08-04 18:05:41 +02:00
const accountPrefix = "EXCHANGE-ACCOUNT-";
const accountCredentialsPrefix = "EXCHANGE-ACCOUNTCREDENTIALS-";
2021-08-04 17:14:52 +02:00
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
}
}
2021-08-04 23:16:08 +02:00
if (accounts.size === 0) {
console.log(
"error: No accounts configured (no sections EXCHANGE_ACCOUNT-*).",
);
if (!context.cont) {
console.log("Aborting further checks.");
process.exit(1);
}
}
2021-08-04 17:14:52 +02:00
for (const acc of accounts) {
if (!credentials.has(acc)) {
console.log(
`warning: no credentials configured for exchange-account-${acc}`,
);
}
}
2021-08-04 22:24:06 +02:00
for (const acc of accounts) {
2021-08-04 23:16:08 +02:00
// test credit history
2021-08-04 22:24:06 +02:00
{
const res = await sh(
context,
"su -l --shell /bin/sh " +
2021-08-04 23:16:08 +02:00
`-c 'taler-exchange-wire-gateway-client -s exchange-accountcredentials-${acc} --credit-history'` +
2021-08-04 22:24:06 +02:00
"taler-exchange-wire",
);
if (res.status != 0) {
console.log(res.stdout);
console.log(res.stderr);
console.log(
2021-08-04 23:16:08 +02:00
"error: Could not run taler-exchange-wire-gateway-client. Please review logs above.",
2021-08-04 22:24:06 +02:00
);
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) {
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) {
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);
}
}
2021-08-04 21:58:21 +02:00
}
2021-08-04 22:24:06 +02:00
// Closer
{
const res = await sh(
context,
`su -l --shell /bin/sh -c 'taler-exchange-closer -t' taler-exchange-closer`,
);
if (res.status != 0) {
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);
}
}
}
2021-08-04 17:14:52 +02:00
}
2021-08-04 22:24:06 +02:00
async function checkAggregatorConfig(context: LintContext) {
2021-08-04 21:58:21 +02:00
const res = await sh(
2021-08-04 22:24:06 +02:00
context,
"su -l --shell /bin/sh -c 'taler-exchange-aggregator -t' taler-exchange-aggregator",
2021-08-04 21:58:21 +02:00
);
if (res.status != 0) {
console.log(res.stdout);
console.log(res.stderr);
console.log("error: Could not run aggregator. Please review logs above.");
2021-08-04 22:24:06 +02:00
if (!context.cont) {
console.log("Aborting further checks.");
process.exit(1);
}
2021-08-04 21:58:21 +02:00
}
2021-08-04 17:14:52 +02:00
}
2021-08-04 22:24:06 +02:00
async function checkCloserConfig(context: LintContext) {
const res = await sh(
context,
`su -l --shell /bin/sh -c 'taler-exchange-closer -t' taler-exchange-closer`,
);
2021-08-04 21:58:21 +02:00
if (res.status != 0) {
console.log(res.stdout);
console.log(res.stderr);
2021-08-04 22:24:06 +02:00
console.log("error: Could not run closer. Please review logs above.");
if (!context.cont) {
console.log("Aborting further checks.");
process.exit(1);
}
2021-08-04 21:58:21 +02:00
}
2021-08-04 17:14:52 +02:00
}
2021-08-04 22:24:06 +02:00
function checkMasterPublicKeyConfig(context: LintContext): PubkeyConf {
const cfg = context.cfg;
2021-08-04 17:14:52 +02:00
const pub = cfg.getString("exchange", "master_public_key");
const pubDecoded = decodeCrock(pub.required());
if (pubDecoded.length != 32) {
console.log("error: invalid master public key");
2021-08-04 22:24:06 +02:00
if (!context.cont) {
console.log("Aborting further checks.");
process.exit(1);
}
2021-08-04 17:14:52 +02:00
}
return {
masterPublicKey: pub.required(),
};
}
export async function checkExchangeHttpd(
2021-08-04 22:24:06 +02:00
context: LintContext,
2021-08-04 17:14:52 +02:00
pubConf: PubkeyConf,
): Promise<void> {
2021-08-04 22:24:06 +02:00
const cfg = context.cfg;
2021-08-04 17:14:52 +02:00
const baseUrlEntry = cfg.getString("exchange", "base_url");
if (!baseUrlEntry.isDefined) {
console.log(
"error: configuration needs to specify section EXCHANGE option BASE_URL",
);
process.exit(1);
}
const baseUrl = baseUrlEntry.required();
if (!baseUrl.startsWith("http")) {
console.log(
"error: section EXCHANGE option BASE_URL needs to be an http or https URL",
);
process.exit(1);
}
if (!baseUrl.endsWith("/")) {
console.log(
"error: section EXCHANGE option BASE_URL needs to end with a slash",
);
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`,
);
}
}
2021-08-04 22:43:09 +02:00
// Check if we can use /keys already
2021-08-04 17:14:52 +02:00
{
const keysUrl = new URL("keys", baseUrl);
2021-08-04 21:58:21 +02:00
const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]);
if (!resp) {
console.log(
2021-08-04 22:32:23 +02:00
"error: request to /keys timed out. " +
2021-08-04 21:58:21 +02:00
"Make sure to sign and upload denomination and signing keys " +
"with taler-exchange-offline.",
);
2021-08-04 22:43:09 +02:00
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) {
console.log(
"error: master public key of exchange does not match public key of live exchange",
);
}
2021-08-04 21:58:21 +02:00
}
2021-08-04 22:43:09 +02:00
}
2021-08-04 21:58:21 +02:00
2021-08-04 22:43:09 +02:00
// Check /wire
{
const keysUrl = new URL("wire", baseUrl);
2021-08-04 18:05:41 +02:00
2021-08-04 22:43:09 +02:00
const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]);
if (!resp) {
2021-08-04 18:14:37 +02:00
console.log(
2021-08-04 22:43:09 +02:00
"error: request to /wire timed out. " +
"Make sure to sign and upload accounts and wire fees " +
"using the taler-exchange-offline tool.",
2021-08-04 18:14:37 +02:00
);
2021-08-04 22:43:09 +02:00
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.",
);
}
2021-08-04 18:05:41 +02:00
}
2021-08-04 17:14:52 +02:00
}
}
/**
* Do some basic checks in the configuration of a Taler deployment.
*/
2021-08-04 22:24:06 +02:00
export async function lintExchangeDeployment(
verbose: boolean,
cont: boolean,
): Promise<void> {
if (process.getuid() != 0) {
2021-08-04 17:14:52 +02:00
console.log(
"warning: the exchange deployment linter is designed to be run as root",
);
}
const cfg = Configuration.load();
2021-08-04 22:24:06 +02:00
const context: LintContext = {
cont,
verbose,
cfg,
};
const basic = checkBasicConf(context);
2021-08-04 17:14:52 +02:00
2021-08-04 22:24:06 +02:00
checkCoinConfig(context, basic);
2021-08-04 17:14:52 +02:00
2021-08-04 22:24:06 +02:00
await checkWireConfig(context);
2021-08-04 17:14:52 +02:00
2021-08-04 22:24:06 +02:00
await checkAggregatorConfig(context);
2021-08-04 17:14:52 +02:00
2021-08-04 22:24:06 +02:00
await checkCloserConfig(context);
2021-08-04 17:14:52 +02:00
2021-08-04 22:24:06 +02:00
const pubConf = checkMasterPublicKeyConfig(context);
2021-08-04 17:14:52 +02:00
2021-08-04 22:24:06 +02:00
await checkExchangeHttpd(context, pubConf);
2021-08-02 16:23:17 +02:00
}