UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

520 lines (519 loc) 20.5 kB
/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* * Copyright 2014-2015 Fabian Tollenaar <fabian@starting-point.nl> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.load = load; exports.readDefaultsFile = readDefaultsFile; exports.sendBaseDeltas = sendBaseDeltas; exports.writeDefaultsFile = writeDefaultsFile; exports.writeBaseDeltasFileSync = writeBaseDeltasFileSync; exports.writeBaseDeltasFile = writeBaseDeltasFile; exports.writeSettingsFile = writeSettingsFile; const fs_1 = __importDefault(require("fs")); const node_os_1 = __importDefault(require("node:os")); const lodash_1 = __importDefault(require("lodash")); const path_1 = __importDefault(require("path")); const semver_1 = __importDefault(require("semver")); const uuid_1 = require("uuid"); const debug_1 = require("../debug"); const deltaeditor_1 = __importDefault(require("../deltaeditor")); const ports_1 = require("../ports"); const atomicWrite_1 = require("../atomicWrite"); const priorities_file_1 = require("./priorities-file"); const unitpreferences_1 = require("../unitpreferences"); const debug = (0, debug_1.createDebug)('signalk-server:config'); let disableWriteSettings = false; // use dynamic path so that ts compiler does not detect this // json file, as ts compile needs to copy all (other) used // json files under /dist // tslint:disable-next-line const packageJson = require('../../' + 'package.json'); function load(app) { app.argv = require('minimist')(process.argv.slice(2)); const config = (app.config = app.config || {}); const env = (app.env = process.env); config.getExternalHostname = getExternalHostname.bind(config, config); config.getExternalPort = ports_1.getExternalPort.bind(config, app); config.isExternalSsl = isExternalSsl.bind(null, config); config.appPath = config.appPath || path_1.default.normalize(__dirname + '/../../'); debug('appPath:' + config.appPath); try { config.name = packageJson.name; config.author = packageJson.author; config.contributors = packageJson.contributors; config.version = packageJson.version; config.description = packageJson.description; //if dependencies are installed from tarballs like in //master docker build the version will be like //file:signalk-server-admin-ui-1.44.1.tgz if (!process.env.SKIP_ADMINUI_VERSION_CHECK) { checkPackageVersion('@signalk/server-admin-ui', packageJson, app.config.appPath); } } catch (err) { console.error('error parsing package.json', err); process.exit(1); } setConfigDirectory(app); app.config.baseDeltaEditor = new deltaeditor_1.default(); if (lodash_1.default.isObject(app.config.settings)) { debug('Using settings from constructor call, not reading defaults'); disableWriteSettings = true; if (config.defaults) { convertOldDefaultsToDeltas(app.config.baseDeltaEditor, config.defaults); } } else { readSettingsFile(app); if (!setBaseDeltas(app)) { const defaults = getFullDefaults(app); if (defaults) { convertOldDefaultsToDeltas(app.config.baseDeltaEditor, defaults); if (typeof app.config.settings.useBaseDeltas === 'undefined' || app.config.settings.useBaseDeltas) { writeBaseDeltasFileSync(app); } else { app.config.hasOldDefaults = true; } } } } setSelfSettings(app); // TRUST_PROXY env var overrides settings file — useful for container deployments if (process.env.TRUST_PROXY !== undefined) { const envVal = process.env.TRUST_PROXY; app.config.settings.trustProxy = envVal === 'true' ? true : envVal === 'false' ? false : isNaN(Number(envVal)) ? envVal : Number(envVal); } // Load unit preferences try { (0, unitpreferences_1.setApplicationDataPath)(app.config.configPath); (0, unitpreferences_1.loadAll)(); debug('Unit preferences loaded'); } catch (err) { console.error('Failed to load unit preferences:', err); // Non-fatal - server can run without unit preferences } if (app.argv['sample-nmea0183-data']) { const sample = path_1.default.join(app.config.appPath, 'samples/plaka.log'); console.log(`Using sample data from ${sample}`); app.config.settings.pipedProviders.push({ id: 'nmea0183-sample-data', pipeElements: [ { type: 'providers/simple', options: { logging: false, type: 'FileStream', subOptions: { dataType: 'NMEA0183', filename: sample } } } ], enabled: true }); } if (app.argv['sample-n2k-data']) { const sample = path_1.default.join(app.config.appPath, 'samples/aava-n2k.data'); console.log(`Using sample data from ${sample}`); app.config.settings.pipedProviders.push({ id: 'n2k-sample-data', pipeElements: [ { type: 'providers/simple', options: { logging: false, type: 'FileStream', subOptions: { dataType: 'NMEA2000JS', filename: sample } } } ], enabled: true }); } if (app.argv['override-timestamps']) { app.config.overrideTimestampWithNow = true; } if (app.argv.securityenabled && !app.config.security) { app.config.settings.security = { strategy: './tokensecurity' }; } if (env.SSLPORT) { config.settings.ssl = true; } if (!lodash_1.default.isUndefined(app.env.WSCOMPRESSION)) { config.settings.wsCompression = app.env.WSCOMPRESSION.toLowerCase() === 'true'; } if (config.settings.landingPage && config.settings.landingPage.charAt(0) !== '/') { console.error(`invalid rootUri: ${config.settings.landingPage}`); process.exit(1); } require('./development')(app); require('./production')(app); } function checkPackageVersion(name, pkg, appPath) { const isOptional = Boolean(pkg.optionalDependencies?.[name]); const expected = pkg.dependencies?.[name] ?? pkg.optionalDependencies?.[name]; if (!expected) { return; } let modulePackageJsonPath = path_1.default.join(appPath, 'node_modules', name, 'package.json'); if (!fs_1.default.existsSync(modulePackageJsonPath)) { modulePackageJsonPath = path_1.default.join(appPath, '..', name, 'package.json'); } if (!fs_1.default.existsSync(modulePackageJsonPath) && isOptional) { // Optional package not installed (e.g. core image with --omit=optional). return; } const installed = require(modulePackageJsonPath); if (!semver_1.default.satisfies(installed.version, expected)) { console.error(`invalid version of the ${name} package is installed ${installed.version} !== ${expected}`); process.exit(1); } } // Establish what the config directory path is. function getConfigDirectory({ argv, config, env }) { // Possible paths in order of priority. const configPaths = [ env.SIGNALK_NODE_CONDFIG_DIR, env.SIGNALK_NODE_CONFIG_DIR, config.configPath, argv.c, argv.s && config.appPath, env.HOME && path_1.default.join(env.HOME, '.signalk'), config.appPath ]; // Find first config directory path that has a truthy value. const configPath = path_1.default.resolve(lodash_1.default.find(configPaths)); debug('configDirPath: ' + configPath); return configPath; } // Create directories and set app.config.configPath. function setConfigDirectory(app) { app.config.configPath = getConfigDirectory(app); if (app.config.configPath !== app.config.appPath) { if (!fs_1.default.existsSync(app.config.configPath)) { fs_1.default.mkdirSync(app.config.configPath); debug(`configDir Created: ${app.config.configPath}`); } const configPackage = path_1.default.join(app.config.configPath, 'package.json'); if (!fs_1.default.existsSync(configPackage)) { fs_1.default.writeFileSync(configPackage, JSON.stringify(pluginsPackageJsonTemplate, null, 2)); } const npmrcPath = path_1.default.join(app.config.configPath, '.npmrc'); if (!fs_1.default.existsSync(npmrcPath)) { fs_1.default.writeFileSync(npmrcPath, 'package-lock=false\n'); } else { const contents = fs_1.default.readFileSync(npmrcPath); if (contents.indexOf('package-lock=') === -1) { fs_1.default.appendFileSync(npmrcPath, '\npackage-lock=false\n'); } } } } function getDefaultsPath(app) { const defaultsFile = app.config.configPath !== app.config.appPath ? 'defaults.json' : 'settings/defaults.json'; return path_1.default.join(app.config.configPath, defaultsFile); } function getBaseDeltasPath(app) { const defaultsFile = app.config.configPath !== app.config.appPath ? 'baseDeltas.json' : 'settings/baseDeltas.json'; return path_1.default.join(app.config.configPath, defaultsFile); } function readDefaultsFile(app) { const defaultsPath = getDefaultsPath(app); const data = fs_1.default.readFileSync(defaultsPath); return JSON.parse(data.toString()); } function getFullDefaults(app) { const defaultsPath = getDefaultsPath(app); try { const defaults = readDefaultsFile(app); debug(`Found defaults at ${defaultsPath.toString()}`); return defaults; } catch (e) { if (e?.code === 'ENOENT') { return undefined; } else { console.error(`unable to parse ${defaultsPath.toString()}`); console.error(e); process.exit(1); } } return undefined; } function setBaseDeltas(app) { const defaultsPath = getBaseDeltasPath(app); try { app.config.baseDeltaEditor.load(defaultsPath); debug(`Found default deltas at ${defaultsPath.toString()}`); } catch (e) { if (e?.code === 'ENOENT') { debug(`No default deltas found at ${defaultsPath.toString()}`); return; } else { console.log(e); } } return true; } function sendBaseDeltas(app) { const copy = JSON.parse(JSON.stringify(app.config.baseDeltaEditor.deltas)); copy.forEach((delta) => { app.handleMessage('defaults', delta); }); } function writeDefaultsFile(app, defaults, cb) { (0, atomicWrite_1.atomicWriteFile)(getDefaultsPath(app), JSON.stringify(defaults, null, 2)) .then(() => cb()) .catch(cb); } function writeBaseDeltasFileSync(app) { app.config.baseDeltaEditor.saveSync(getBaseDeltasPath(app)); } function writeBaseDeltasFile(app) { return app.config.baseDeltaEditor.save(getBaseDeltasPath(app)); } function setSelfSettings(app) { const name = app.config.baseDeltaEditor.getSelfValue('name'); const mmsi = app.config.baseDeltaEditor.getSelfValue('mmsi'); let uuid = app.config.baseDeltaEditor.getSelfValue('uuid'); if (mmsi && !lodash_1.default.isString(mmsi)) { throw new Error(`invalid mmsi: ${mmsi}`); } if (uuid && !lodash_1.default.isString(uuid)) { throw new Error(`invalid uuid: ${uuid}`); } if (mmsi === null && uuid === null) { uuid = 'urn:mrn:signalk:uuid:' + (0, uuid_1.v4)(); app.config.baseDeltaEditor.setSelfValue('uuid', uuid); } app.config.vesselName = name; if (mmsi) { app.selfType = 'mmsi'; app.selfId = 'urn:mrn:imo:mmsi:' + mmsi; app.config.vesselMMSI = mmsi; } else if (uuid) { app.selfType = 'uuid'; app.selfId = uuid; app.config.vesselUUID = uuid; } if (app.selfType) { debug(app.selfType.toUpperCase() + ': ' + app.selfId); } app.selfContext = 'vessels.' + app.selfId; } function readSettingsFile(app) { const settings = getSettingsFilename(app); if (!app.argv.s && !fs_1.default.existsSync(settings)) { console.log('Settings file does not exist, using empty settings'); app.config.settings = { pipedProviders: [] }; } else { debug('Using settings file: ' + settings); try { app.config.settings = require(settings); } catch (_e) { console.error(`Error reading settings file ${settings}, using empty settings`); app.config.settings = { pipedProviders: [] }; } } if (lodash_1.default.isUndefined(app.config.settings.pipedProviders)) { app.config.settings.pipedProviders = []; } if (lodash_1.default.isUndefined(app.config.settings.interfaces)) { app.config.settings.interfaces = {}; } (0, priorities_file_1.loadPrioritiesIntoSettings)(app); const migrated = (0, priorities_file_1.migratePrioritiesIntoSeparateFile)(app); if (migrated && !disableWriteSettings) { // Persist settings.json without the priority keys so it stays clean. (0, atomicWrite_1.atomicWriteFile)(getSettingsFilename(app), JSON.stringify(app.config.settings, null, 2)).catch((e) => { console.error('Failed to strip migrated priority keys from settings.json:', e); }); } } function writeSettingsFile(app, settings, cb) { if (disableWriteSettings) { cb(); return; } const { settingsWithoutPriorities, priorities } = (0, priorities_file_1.splitPrioritiesFromSettings)(settings); // Always overwrite priorities.json — when the user resets all priority // state, the in-memory `priorities` is `{}` and the file must be cleared // too, otherwise stale entries from a previous save reload on next start. // // Sequence the writes (priorities first, then settings) so that if the // priorities write fails, settings.json is left untouched — on restart // the loader still has legacy priority keys to fold back in. If the // settings write fails after priorities succeeded, only the // non-priority slice is stale, which is the safer half: the engine // keeps using the just-saved priorities.json on next load. (0, priorities_file_1.writePrioritiesFile)(app, priorities) .then(() => (0, atomicWrite_1.atomicWriteFile)(getSettingsFilename(app), JSON.stringify(settingsWithoutPriorities, null, 2))) .then(() => cb()) .catch(cb); } function getSettingsFilename(app) { if (process.env.SIGNALK_NODE_SETTINGS) { debug('Settings filename was set in environment SIGNALK_NODE_SETTINGS, overriding all other options'); return path_1.default.resolve(process.env.SIGNALK_NODE_SETTINGS); } const settingsFile = app.argv.s || 'settings.json'; return path_1.default.join(app.config.configPath, settingsFile); } function isExternalSsl(config) { if (process.env.EXTERNALSSL) { return (process.env.EXTERNALSSL === '1' || process.env.EXTERNALSSL.toLowerCase() === 'true'); } return !!config.settings.proxy_ssl; } function getExternalHostname(config) { if (process.env.EXTERNALHOST) { return process.env.EXTERNALHOST; } if (config.settings.proxy_host) { return config.settings.proxy_host; } else if (config.settings.hostname) { return config.settings.hostname; } try { return node_os_1.default.hostname(); } catch (_ex) { return 'hostname_not_available'; } } function scanDefaults(deltaEditor, vpath, item) { lodash_1.default.keys(item).forEach((key) => { const value = item[key]; if (key === 'meta') { deltaEditor.setMeta('vessels.self', vpath, value); } else if (key === 'value') { deltaEditor.setSelfValue(vpath, value); } else if (lodash_1.default.isObject(value)) { const childPath = vpath.length > 0 ? `${vpath}.${key}` : key; scanDefaults(deltaEditor, childPath, value); } }); } // Walks an object emitted under a parent path and recursively sets a // leaf delta per terminal value (strings, numbers, booleans, arrays). // Used for legacy defaults.json shapes that nest bare values directly // (e.g. `communication: { callsignVhf: "OH..." }`) — scanDefaults only // reads `{ value: ... }` leaves so it silently drops bare values, and // emitting the whole object at the parent path leaks an out-of-date // snapshot of every child every time anyone GETs the parent. function emitBareLeafDeltas(deltaEditor, parentPath, item) { if (!lodash_1.default.isPlainObject(item)) return; const obj = item; for (const key of Object.keys(obj)) { const value = obj[key]; const childPath = parentPath.length > 0 ? `${parentPath}.${key}` : key; if (lodash_1.default.isPlainObject(value)) { // Skip {value:..., meta:...} shapes — scanDefaults already handled // those in the main pass. Recurse into anything else to reach the // bare leaves below. const child = value; if ('value' in child || 'meta' in child) continue; emitBareLeafDeltas(deltaEditor, childPath, child); } else if (value !== undefined && value !== null) { deltaEditor.setSelfValue(childPath, value); } } } function convertOldDefaultsToDeltas(deltaEditor, defaults) { const deltas = []; const self = lodash_1.default.get(defaults, 'vessels.self'); if (self) { lodash_1.default.keys(self).forEach((key) => { const value = self[key]; if (!lodash_1.default.isString(value)) { scanDefaults(deltaEditor, key, value); } else { deltaEditor.setSelfValue(key, value); } }); if (self.communication) { // Legacy shape stores `communication.*` as bare values, not the // `{ value: ... }` shape scanDefaults walks. Emit them as separate // leaf deltas so they don't collide with paths the spec keeps // under `communication` (e.g. communication.crewNames written by // signalk-logbook), which a parent-path snapshot would otherwise // serve as stale, defaults-sourced JSON in the Data Browser. emitBareLeafDeltas(deltaEditor, 'communication', self.communication); } } return deltas; } const pluginsPackageJsonTemplate = { name: 'signalk-server-config', version: '0.0.1', description: 'This file is here to track your plugin and webapp installs.', repository: {}, license: 'Apache-2.0' }; module.exports = { load, getConfigDirectory, writeSettingsFile, writeDefaultsFile, readDefaultsFile, sendBaseDeltas, writeBaseDeltasFile, package: packageJson }; //# sourceMappingURL=config.js.map