summaryrefslogtreecommitdiff
path: root/packages/taler-util/src/talerconfig.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-util/src/talerconfig.ts')
-rw-r--r--packages/taler-util/src/talerconfig.ts350
1 files changed, 296 insertions, 54 deletions
diff --git a/packages/taler-util/src/talerconfig.ts b/packages/taler-util/src/talerconfig.ts
index 59c789cae..2bd7b355f 100644
--- a/packages/taler-util/src/talerconfig.ts
+++ b/packages/taler-util/src/talerconfig.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
+ (C) 2020-2023 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
@@ -23,12 +23,14 @@
/**
* Imports
*/
-import { AmountJson } from "./amounts.js";
-import { Amounts } from "./amounts.js";
+import { AmountJson, Amounts } from "./amounts.js";
+import { Logger } from "./logging.js";
-import nodejs_path from "path";
-import nodejs_os from "os";
import nodejs_fs from "fs";
+import nodejs_os from "os";
+import nodejs_path from "path";
+
+const logger = new Logger("talerconfig.ts");
export class ConfigError extends Error {
constructor(message: string) {
@@ -39,10 +41,30 @@ export class ConfigError extends Error {
}
}
+enum EntryOrigin {
+ /**
+ * From a default file.
+ */
+ DefaultFile = 1,
+ /**
+ * From a system/installation specific default value.
+ */
+ DefaultSystem = 2,
+ /**
+ * Loaded from file or string
+ */
+ Loaded = 3,
+ /**
+ * Changed after loading
+ */
+ Changed = 4,
+}
+
interface Entry {
value: string;
sourceLine: number;
sourceFile: string;
+ origin: EntryOrigin;
}
interface Section {
@@ -53,11 +75,59 @@ interface Section {
type SectionMap = { [sectionName: string]: Section };
+/**
+ * Different projects use the GNUnet/Taler-Style config.
+ *
+ * The config source determines where to locate the configuration.
+ */
+export interface ConfigSource {
+ projectName: string;
+ componentName: string;
+ installPathBinary: string;
+ baseConfigVarname: string;
+ prefixVarname: string;
+}
+
+export type ConfigSourceDef = { [x: string]: ConfigSource | undefined };
+
+export const ConfigSources = {
+ ["taler"]: {
+ projectName: "taler",
+ componentName: "taler",
+ installPathBinary: "taler-config",
+ baseConfigVarname: "TALER_BASE_CONFIG",
+ prefixVarname: "TALER_PREFIX",
+ } satisfies ConfigSource,
+ ["libeufin-bank"]: {
+ projectName: "libeufin",
+ componentName: "libeufin-bank",
+ installPathBinary: "libeufin-bank",
+ baseConfigVarname: "LIBEUFIN_BASE_CONFIG",
+ prefixVarname: "LIBEUFIN_PREFIX",
+ } satisfies ConfigSource,
+ ["libeufin-nexus"]: {
+ projectName: "libeufin",
+ componentName: "libeufin-nexus",
+ installPathBinary: "libeufin-nexus",
+ baseConfigVarname: "LIBEUFIN_BASE_CONFIG",
+ prefixVarname: "LIBEUFIN_PREFIX",
+ } satisfies ConfigSource,
+ ["gnunet"]: {
+ projectName: "gnunet",
+ componentName: "gnunet",
+ installPathBinary: "gnunet-config",
+ baseConfigVarname: "GNUNET_BASE_CONFIG",
+ prefixVarname: "GNUNET_PREFIX",
+ } satisfies ConfigSource,
+} satisfies ConfigSourceDef;
+
+const defaultConfigSource: ConfigSource = ConfigSources.taler;
+
export class ConfigValue<T> {
constructor(
private sectionName: string,
private optionName: string,
- public value: string | undefined,
+ private value: string | undefined,
private converter: (x: string) => T,
) {}
@@ -89,6 +159,10 @@ export class ConfigValue<T> {
isDefined(): boolean {
return this.value !== undefined;
}
+
+ getValue(): string | undefined {
+ return this.value;
+ }
}
/**
@@ -116,9 +190,9 @@ export function expandPath(path: string): string {
export function pathsub(
x: string,
lookup: (s: string, depth: number) => string | undefined,
- depth = 0,
+ recursionDepth = 0,
): string {
- if (depth >= 10) {
+ if (recursionDepth >= 128) {
throw Error("recursion in path substitution");
}
let s = x;
@@ -157,14 +231,14 @@ export function pathsub(
defaultValue = undefined;
}
- const r = lookup(inner, depth + 1);
+ const r = lookup(varname, depth + 1);
if (r !== undefined) {
- s = s.substr(0, start) + r + s.substr(p + 1);
+ s = s.substring(0, start) + r + s.substring(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);
+ s = s.substring(0, start) + resolvedDefault + s.substring(p + 1);
l = start + resolvedDefault.length;
continue;
}
@@ -174,9 +248,9 @@ export function pathsub(
} 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);
+ const r = lookup(m[0], recursionDepth + 1);
if (r !== undefined) {
- s = s.substr(0, l) + r + s.substr(l + 1 + m[0].length);
+ s = s.substring(0, l) + r + s.substring(l + 1 + m[0].length);
l = l + r.length;
continue;
}
@@ -188,13 +262,14 @@ export function pathsub(
return s;
}
-export interface LoadOptions {
+interface LoadOptions {
filename?: string;
banDirectives?: boolean;
}
export interface StringifyOptions {
diagnostics?: boolean;
+ excludeDefaults?: boolean;
}
export interface LoadedFile {
@@ -282,7 +357,19 @@ export class Configuration {
private nestLevel = 0;
- private loadFromFilename(filename: string, opts: LoadOptions = {}): void {
+ /**
+ * Does the entrypoint config file contain complex
+ * directives?
+ */
+ private entrypointIsComplex: boolean = false;
+
+ constructor(private configSource: ConfigSource = defaultConfigSource) {}
+
+ private loadFromFilename(
+ filename: string,
+ isDefaultSource: boolean,
+ opts: LoadOptions = {},
+ ): void {
filename = expandPath(filename);
const checkCycle = () => {
@@ -309,7 +396,7 @@ export class Configuration {
const oldNestLevel = this.nestLevel;
this.nestLevel += 1;
try {
- this.loadFromString(s, {
+ this.internalLoadFromString(s, isDefaultSource, {
...opts,
filename: filename,
});
@@ -318,7 +405,11 @@ export class Configuration {
}
}
- private loadGlob(parentFilename: string, fileglob: string): void {
+ private loadGlob(
+ parentFilename: string,
+ isDefaultSource: boolean,
+ fileglob: string,
+ ): void {
const resolvedParent = nodejs_fs.realpathSync(parentFilename);
const parentDir = nodejs_path.dirname(resolvedParent);
@@ -339,12 +430,16 @@ export class Configuration {
for (const f of files) {
if (globMatch(tail, f)) {
const fullPath = nodejs_path.join(head, f);
- this.loadFromFilename(fullPath);
+ this.loadFromFilename(fullPath, isDefaultSource);
}
}
}
- private loadSecret(sectionName: string, filename: string): void {
+ private loadSecret(
+ sectionName: string,
+ filename: string,
+ isDefaultSource: boolean,
+ ): void {
const sec = this.provideSection(sectionName);
sec.secretFilename = filename;
const otherCfg = new Configuration();
@@ -354,7 +449,7 @@ export class Configuration {
sec.inaccessible = true;
return;
}
- otherCfg.loadFromFilename(filename, {
+ otherCfg.loadFromFilename(filename, isDefaultSource, {
banDirectives: true,
});
const otherSec = otherCfg.provideSection(sectionName);
@@ -363,7 +458,11 @@ export class Configuration {
}
}
- loadFromString(s: string, opts: LoadOptions = {}): void {
+ private internalLoadFromString(
+ s: string,
+ isDefaultSource: boolean,
+ opts: LoadOptions = {},
+ ): void {
let lineNo = 0;
const fn = opts.filename ?? "<input>";
const reComment = /^\s*#.*$/;
@@ -390,6 +489,9 @@ export class Configuration {
`invalid configuration, directive in ${fn}:${lineNo} forbidden`,
);
}
+ if (!isDefaultSource) {
+ this.entrypointIsComplex = true;
+ }
const directive = directiveMatch[1].toLowerCase();
switch (directive) {
case "inline": {
@@ -399,7 +501,10 @@ export class Configuration {
);
}
const arg = directiveMatch[2].trim();
- this.loadFromFilename(normalizeInlineFilename(opts.filename, arg));
+ this.loadFromFilename(
+ normalizeInlineFilename(opts.filename, arg),
+ isDefaultSource,
+ );
break;
}
case "inline-secret": {
@@ -419,7 +524,7 @@ export class Configuration {
opts.filename,
sp[1],
);
- this.loadSecret(sp[0], secretFilename);
+ this.loadSecret(sp[0], secretFilename, isDefaultSource);
break;
}
case "inline-matching": {
@@ -429,7 +534,7 @@ export class Configuration {
`invalid configuration, @inline-matching@ directive in ${fn}:${lineNo} can only be used from a file`,
);
}
- this.loadGlob(opts.filename, arg);
+ this.loadGlob(opts.filename, isDefaultSource, arg);
break;
}
default:
@@ -462,6 +567,9 @@ export class Configuration {
value: val,
sourceFile: opts.filename ?? "<unknown>",
sourceLine: lineNo,
+ origin: isDefaultSource
+ ? EntryOrigin.DefaultFile
+ : EntryOrigin.Loaded,
};
continue;
}
@@ -496,6 +604,24 @@ export class Configuration {
value,
sourceLine: 0,
sourceFile: "<unknown>",
+ origin: EntryOrigin.Changed,
+ };
+ }
+
+ /**
+ * Set a string value to a value from default locations.
+ */
+ private setStringSystemDefault(
+ section: string,
+ option: string,
+ value: string,
+ ): void {
+ const sec = this.provideSection(section);
+ sec.entries[option.toUpperCase()] = {
+ value,
+ sourceLine: 0,
+ sourceFile: "<unknown>",
+ origin: EntryOrigin.DefaultSystem,
};
}
@@ -563,11 +689,14 @@ export class Configuration {
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;
}
+
+ logger.warn(`unable to resolve variable '${x}'`);
return;
}
@@ -578,71 +707,146 @@ export class Configuration {
);
}
- loadFrom(dirname: string): void {
+ private loadDefaultsFromDir(dirname: string): void {
const files = nodejs_fs.readdirSync(dirname);
for (const f of files) {
const fn = nodejs_path.join(dirname, f);
- this.loadFromFilename(fn);
+ this.loadFromFilename(fn, true);
}
}
private loadDefaults(): void {
- let bc = process.env["TALER_BASE_CONFIG"];
- if (!bc) {
+ const { projectName, prefixVarname, baseConfigVarname, installPathBinary } =
+ this.configSource;
+ let baseConfigDir = process.env[baseConfigVarname];
+ if (!baseConfigDir) {
/* Try to locate the configuration based on the location
* of the taler-config binary. */
- const path = which("taler-config");
+ const path = which(installPathBinary);
+ if (path) {
+ baseConfigDir = nodejs_fs.realpathSync(
+ nodejs_path.dirname(path) + `/../share/${projectName}/config.d`,
+ );
+ }
+ }
+ if (!baseConfigDir) {
+ baseConfigDir = `/usr/share/${projectName}/config.d`;
+ }
+
+ let installPrefix = process.env[prefixVarname];
+ if (!installPrefix) {
+ /* Try to locate install path based on the location
+ * of the taler-config binary. */
+ const path = which(installPathBinary);
if (path) {
- bc = nodejs_fs.realpathSync(
- nodejs_path.dirname(path) + "/../share/taler/config.d",
+ installPrefix = nodejs_fs.realpathSync(
+ nodejs_path.dirname(path) + "/..",
);
}
}
- if (!bc) {
- bc = "/usr/share/taler/config.d";
+ if (!installPrefix) {
+ installPrefix = "/usr";
}
- this.loadFrom(bc);
+
+ this.setStringSystemDefault(
+ "PATHS",
+ "LIBEXECDIR",
+ `${installPrefix}/${projectName}/libexec/`,
+ );
+ this.setStringSystemDefault(
+ "PATHS",
+ "DOCDIR",
+ `${installPrefix}/share/doc/${projectName}/`,
+ );
+ this.setStringSystemDefault(
+ "PATHS",
+ "ICONDIR",
+ `${installPrefix}/share/icons/`,
+ );
+ this.setStringSystemDefault(
+ "PATHS",
+ "LOCALEDIR",
+ `${installPrefix}/share/locale/`,
+ );
+ this.setStringSystemDefault("PATHS", "PREFIX", `${installPrefix}/`);
+ this.setStringSystemDefault("PATHS", "BINDIR", `${installPrefix}/bin`);
+ this.setStringSystemDefault(
+ "PATHS",
+ "LIBDIR",
+ `${installPrefix}/lib/${projectName}/`,
+ );
+ this.setStringSystemDefault(
+ "PATHS",
+ "DATADIR",
+ `${installPrefix}/share/${projectName}/`,
+ );
+
+ this.loadDefaultsFromDir(baseConfigDir);
}
- getDefaultConfigFilename(): string | undefined {
+ private findDefaultConfigFilename(): string | undefined {
const xdg = process.env["XDG_CONFIG_HOME"];
const home = process.env["HOME"];
let fn: string | undefined;
+ const { projectName, componentName } = this.configSource;
if (xdg) {
- fn = nodejs_path.join(xdg, "taler.conf");
+ fn = nodejs_path.join(xdg, `${componentName}.conf`);
} else if (home) {
- fn = nodejs_path.join(home, ".config/taler.conf");
+ fn = nodejs_path.join(home, `.config/${componentName}.conf`);
}
if (fn && nodejs_fs.existsSync(fn)) {
return fn;
}
- const etc1 = "/etc/taler.conf";
+ const etc1 = `/etc/${componentName}.conf`;
if (nodejs_fs.existsSync(etc1)) {
return etc1;
}
- const etc2 = "/etc/taler/taler.conf";
+ const etc2 = `/etc/${projectName}/${componentName}.conf`;
if (nodejs_fs.existsSync(etc2)) {
return etc2;
}
return undefined;
}
- static load(filename?: string): Configuration {
- const cfg = new Configuration();
+ static load(
+ filename?: string,
+ configSource?: ConfigSource | string,
+ ): Configuration {
+ let cs: ConfigSource;
+ if (configSource == null) {
+ cs = defaultConfigSource;
+ } else if (typeof configSource === "string") {
+ if (configSource in ConfigSources) {
+ cs = ConfigSources[configSource as keyof typeof ConfigSources];
+ } else {
+ throw Error("invalid config source");
+ }
+ } else {
+ cs = configSource;
+ }
+ const cfg = new Configuration(cs);
cfg.loadDefaults();
if (filename) {
- cfg.loadFromFilename(filename);
+ cfg.loadFromFilename(filename, false);
+ cfg.hintEntrypoint = filename;
} else {
- const fn = cfg.getDefaultConfigFilename();
+ const fn = cfg.findDefaultConfigFilename();
if (fn) {
- cfg.loadFromFilename(fn);
+ // It's the default filename for the main config file,
+ // but we don't consider the values default values.
+ cfg.loadFromFilename(fn, false);
+ cfg.hintEntrypoint = fn;
}
}
- cfg.hintEntrypoint = filename;
return cfg;
}
stringify(opts: StringifyOptions = {}): string {
+ if (opts.excludeDefaults && this.entrypointIsComplex) {
+ throw Error(
+ "unable to do diff serialization of config file, as entry point contains complex directives",
+ );
+ }
let s = "";
if (opts.diagnostics) {
s += "# Configuration file diagnostics\n";
@@ -657,26 +861,64 @@ export class Configuration {
}
for (const sectionName of Object.keys(this.sectionMap)) {
const sec = this.sectionMap[sectionName];
- if (opts.diagnostics && sec.secretFilename) {
- s += `# Secret section from ${sec.secretFilename}\n`;
- s += `# Secret accessible: ${!sec.inaccessible}\n`;
- }
- s += `[${sectionName}]\n`;
+ let headerWritten = false;
for (const optionName of Object.keys(sec.entries)) {
const entry = this.sectionMap[sectionName].entries[optionName];
+ if (
+ opts.excludeDefaults &&
+ (entry.origin === EntryOrigin.DefaultSystem ||
+ entry.origin === EntryOrigin.DefaultFile)
+ ) {
+ continue;
+ }
+ if (!headerWritten) {
+ if (opts.diagnostics && sec.secretFilename) {
+ s += `# Secret section from ${sec.secretFilename}\n`;
+ s += `# Secret accessible: ${!sec.inaccessible}\n`;
+ }
+ s += `[${sectionName}]\n`;
+ headerWritten = true;
+ }
if (entry !== undefined) {
if (opts.diagnostics) {
- s += `# ${entry.sourceFile}:${entry.sourceLine}\n`;
+ switch (entry.origin) {
+ case EntryOrigin.DefaultFile:
+ case EntryOrigin.Changed:
+ case EntryOrigin.Loaded:
+ s += `# ${entry.sourceFile}:${entry.sourceLine}\n`;
+ break;
+ case EntryOrigin.DefaultSystem:
+ s += `# (system/installation default)\n`;
+ break;
+ }
}
s += `${optionName} = ${entry.value}\n`;
}
}
- s += "\n";
+ if (headerWritten) {
+ s += "\n";
+ }
}
return s;
}
- write(filename: string): void {
- nodejs_fs.writeFileSync(filename, this.stringify());
+ write(opts: { excludeDefaults?: boolean } = {}): void {
+ const filename = this.hintEntrypoint;
+ if (!filename) {
+ throw Error(
+ "unknown configuration entrypoing, unable to write back config file",
+ );
+ }
+ nodejs_fs.writeFileSync(
+ filename,
+ this.stringify({ excludeDefaults: opts.excludeDefaults }),
+ );
+ }
+
+ writeTo(filename: string, opts: { excludeDefaults?: boolean } = {}): void {
+ nodejs_fs.writeFileSync(
+ filename,
+ this.stringify({ excludeDefaults: opts.excludeDefaults }),
+ );
}
}