"use strict";
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-this-alias */
/*
 * 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.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
require("./baconjs-compat");
const server_api_1 = require("@signalk/server-api");
const signalk_schema_1 = require("@signalk/signalk-schema");
const express_1 = __importDefault(require("express"));
const http_1 = __importDefault(require("http"));
const https_1 = __importDefault(require("https"));
const lodash_1 = __importDefault(require("lodash"));
const path_1 = __importDefault(require("path"));
const api_1 = require("./api");
const config_1 = require("./config/config");
const debug_1 = require("./debug");
const deltacache_1 = __importDefault(require("./deltacache"));
const deltachain_1 = __importDefault(require("./deltachain"));
const deltaPriority_1 = require("./deltaPriority");
const staticDataFilter_1 = require("./staticDataFilter");
const deltastats_1 = require("./deltastats");
const modules_1 = require("./modules");
const ports_1 = require("./ports");
const security_js_1 = require("./security.js");
const cors_1 = require("./cors");
const subscriptionmanager_1 = __importDefault(require("./subscriptionmanager"));
const pipedproviders_1 = require("./pipedproviders");
const events_1 = require("./events");
const zones_1 = require("./zones");
const version_1 = __importDefault(require("./version"));
const helmet_1 = __importDefault(require("helmet"));
const debug = (0, debug_1.createDebug)('signalk-server');
const streambundle_1 = require("./streambundle");
class Server {
    app;
    constructor(opts) {
        (0, version_1.default)();
        const FILEUPLOADSIZELIMIT = process.env.FILEUPLOADSIZELIMIT || '10mb';
        const bodyParser = require('body-parser');
        const app = (0, express_1.default)();
        app.use(require('compression')());
        app.use((0, helmet_1.default)({
            // ENABLED (safe, no compatibility impact):
            xContentTypeOptions: true,
            xDnsPrefetchControl: true,
            xDownloadOptions: true,
            xPermittedCrossDomainPolicies: true,
            referrerPolicy: true,
            hsts: true,
            // DISABLED (would break chart plotters, plugins, webapps):
            frameguard: false, // Allow embedding in iframes (chart plotters, MFDs)
            contentSecurityPolicy: false,
            crossOriginEmbedderPolicy: false,
            crossOriginOpenerPolicy: false,
            crossOriginResourcePolicy: false
        }));
        app.use(bodyParser.json({ limit: FILEUPLOADSIZELIMIT }));
        this.app = app;
        app.started = false;
        lodash_1.default.merge(app, opts);
        (0, config_1.load)(app);
        // Apply trust proxy setting if configured
        if (app.config.settings.trustProxy !== undefined) {
            app.set('trust proxy', app.config.settings.trustProxy);
        }
        app.logging = require('./logging')(app);
        app.version = '0.0.1';
        (0, cors_1.setupCors)(app, (0, security_js_1.getSecurityConfig)(app));
        (0, security_js_1.startSecurity)(app, opts ? opts.securityConfig : null);
        require('./serverroutes')(app, security_js_1.saveSecurityConfig, security_js_1.getSecurityConfig);
        require('./put').start(app);
        app.signalk = new signalk_schema_1.FullSignalK(app.selfId, app.selfType);
        const maxListeners = process.env.EVENT_EMITTER_MAX_LISTENERS
            ? parseInt(process.env.EVENT_EMITTER_MAX_LISTENERS, 10)
            : 50;
        if (maxListeners > 0) {
            app.signalk.setMaxListeners(maxListeners);
        }
        app.propertyValues = new server_api_1.PropertyValues();
        const deltachainV1 = new deltachain_1.default(app.signalk.addDelta.bind(app.signalk));
        const deltachainV2 = new deltachain_1.default((delta) => app.signalk.emit('delta', delta));
        app.registerDeltaInputHandler = (handler) => {
            const unRegisterHandlers = [
                deltachainV1.register(handler),
                deltachainV2.register(handler)
            ];
            return () => unRegisterHandlers.forEach((f) => f());
        };
        app.providerStatus = {};
        // feature detection
        app.getFeatures = async (enabled) => {
            return {
                apis: enabled === false ? [] : app.apis,
                plugins: await app.getPluginsList(enabled)
            };
        };
        // create first temporary pluginManager to get typechecks, as
        // app is any and not typechecked
        // TODO separate app.plugins and app.pluginsMap from app
        const pluginManager = {
            setPluginOpenApi: (pluginId, openApi) => {
                app.pluginsMap[pluginId].openApi = openApi;
            },
            getPluginOpenApi: (pluginId) => ({
                name: `plugins/${pluginId}`,
                path: `/plugins/${pluginId}`,
                apiDoc: app.pluginsMap[pluginId].openApi
            }),
            getPluginOpenApiRecords: () => Object.keys(app.pluginsMap).reduce((acc, pluginId) => {
                if (app.pluginsMap[pluginId].openApi) {
                    acc.push({
                        name: `plugins/${pluginId}`,
                        path: `/plugins/${pluginId}`,
                        apiDoc: app.pluginsMap[pluginId].openApi
                    });
                }
                return acc;
            }, [])
        };
        Object.assign(app, pluginManager);
        app.setPluginStatus = (providerId, statusMessage) => {
            doSetProviderStatus(providerId, statusMessage, 'status', 'plugin');
        };
        app.setPluginError = (providerId, errorMessage) => {
            doSetProviderStatus(providerId, errorMessage, 'error', 'plugin');
        };
        app.setProviderStatus = (providerId, statusMessage) => {
            doSetProviderStatus(providerId, statusMessage, 'status');
        };
        app.setProviderError = (providerId, errorMessage) => {
            doSetProviderStatus(providerId, errorMessage, 'error');
        };
        function doSetProviderStatus(providerId, statusMessage, type, statusType = 'provider') {
            if (!statusMessage) {
                delete app.providerStatus[providerId];
                return;
            }
            if (lodash_1.default.isUndefined(app.providerStatus[providerId])) {
                app.providerStatus[providerId] = {};
            }
            const status = app.providerStatus[providerId];
            if (status.type === 'error' && status.message !== statusMessage) {
                status.lastError = status.message;
                status.lastErrorTimeStamp = status.timeStamp;
            }
            status.type = type;
            status.id = providerId;
            status.statusType = statusType;
            status.timeStamp = new Date().toISOString();
            status.message = statusMessage;
            app.emit('serverevent', {
                type: 'PROVIDERSTATUS',
                from: 'signalk-server',
                data: app.getProviderStatus()
            });
        }
        app.getProviderStatus = () => {
            const providerStatus = lodash_1.default.values(app.providerStatus);
            if (app.plugins) {
                app.plugins.forEach((plugin) => {
                    try {
                        if (typeof plugin.statusMessage === 'function' &&
                            lodash_1.default.isUndefined(app.providerStatus[plugin.id])) {
                            let message = plugin.statusMessage();
                            if (message) {
                                message = message.trim();
                                if (message.length > 0) {
                                    providerStatus.push({
                                        message,
                                        type: 'status',
                                        id: plugin.id,
                                        statusType: 'plugin'
                                    });
                                }
                            }
                        }
                    }
                    catch (e) {
                        console.error(e);
                        providerStatus.push({
                            message: 'Error fetching provider status, see server log for details',
                            type: 'status',
                            id: plugin.id
                        });
                    }
                });
            }
            return providerStatus;
        };
        app.registerHistoryProvider = (provider) => {
            app.historyProvider = provider;
        };
        app.unregisterHistoryProvider = () => {
            delete app.historyProvider;
        };
        let toPreferredDelta = () => undefined;
        app.activateSourcePriorities = () => {
            try {
                toPreferredDelta = (0, deltaPriority_1.getToPreferredDelta)(app.config.settings.sourcePriorities);
            }
            catch (e) {
                console.error('getToPreferredDelta failed:', e);
            }
        };
        app.activateSourcePriorities();
        app.handleMessage = (providerId, data, skVersion = server_api_1.SKVersion.v1) => {
            if (data && Array.isArray(data.updates)) {
                (0, deltastats_1.incDeltaStatistics)(app, providerId);
                if (typeof data.context === 'undefined' ||
                    data.context === 'vessels.self') {
                    data.context = ('vessels.' + app.selfId);
                }
                const now = new Date();
                data.updates = data.updates
                    .map((update) => {
                    if (!isValidUpdate(update)) {
                        console.warn(`Discarding update from ${providerId}: invalid values entry (null or missing path)`);
                        return undefined;
                    }
                    if (typeof update.source !== 'undefined') {
                        update.source.label = providerId;
                        if (!update.$source) {
                            update.$source = (0, signalk_schema_1.getSourceId)(update.source);
                        }
                    }
                    else {
                        if (typeof update.$source === 'undefined') {
                            update.$source = providerId;
                        }
                    }
                    if (!update.timestamp || app.config.overrideTimestampWithNow) {
                        update.timestamp = now.toISOString();
                    }
                    if ('meta' in update && !Array.isArray(update.meta)) {
                        debug(`handleMessage: ignoring invalid meta`, update.meta);
                        delete update.meta;
                    }
                    if ('values' in update || 'meta' in update) {
                        return update;
                    }
                })
                    .filter((update) => update !== undefined);
                // No valid updates, discarding
                if (data.updates.length < 1)
                    return;
                try {
                    // data.updates was just rebuilt above, so the resulting Partial<Delta>
                    // is in practice a full Delta by the time it reaches the chain.
                    let delta = (0, staticDataFilter_1.filterStaticSelfData)(data, app.selfContext);
                    delta = toPreferredDelta(delta, now, app.selfContext);
                    if (skVersion === server_api_1.SKVersion.v1) {
                        deltachainV1.process(delta);
                    }
                    else {
                        deltachainV2.process(delta);
                    }
                }
                catch (err) {
                    console.error(err);
                }
            }
        };
        app.streambundle = new streambundle_1.StreamBundle(app.selfId);
        new zones_1.Zones(app.streambundle, (delta) => process.nextTick(() => app.handleMessage('self.notificationhandler', delta)));
        app.signalk.on('delta', app.streambundle.pushDelta.bind(app.streambundle));
        app.subscriptionmanager = new subscriptionmanager_1.default(app);
        app.deltaCache = new deltacache_1.default(app, app.streambundle);
        app.getHello = () => ({
            name: app.config.name,
            version: app.config.version,
            self: `vessels.${app.selfId}`,
            roles: ['master', 'main'],
            timestamp: new Date()
        });
        app.isNmea2000OutAvailable = false;
        app.on('nmea2000OutAvailable', () => {
            app.isNmea2000OutAvailable = true;
        });
        installProcessErrorHandlers(app);
    }
    start() {
        const self = this;
        const app = this.app;
        app.wrappedEmitter = (0, events_1.wrapEmitter)(app);
        app.emit = app.wrappedEmitter.emit;
        app.on = app.wrappedEmitter.addListener;
        app.addListener = app.wrappedEmitter.addListener;
        this.app.intervals = [];
        this.app.intervals.push(setInterval(app.signalk.pruneContexts.bind(app.signalk, (app.config.settings.pruneContextsMinutes || 60) * 60), 60 * 1000));
        this.app.intervals.push(setInterval(app.deltaCache.pruneContexts.bind(app.deltaCache, (app.config.settings.pruneContextsMinutes || 60) * 60), 60 * 1000));
        app.intervals.push(setInterval(() => {
            app.emit('serverevent', {
                type: 'PROVIDERSTATUS',
                from: 'signalk-server',
                data: app.getProviderStatus()
            });
        }, 5 * 1000));
        function serverUpgradeIsAvailable(err, newVersion) {
            if (err) {
                console.error(err);
                return;
            }
            const msg = `A new version (${newVersion}) of the server is available`;
            console.log(msg);
            app.handleMessage(app.config.name, {
                updates: [
                    {
                        values: [
                            {
                                path: 'notifications.server.newVersion',
                                value: {
                                    state: 'normal',
                                    method: [],
                                    message: msg
                                }
                            }
                        ]
                    }
                ]
            });
        }
        if (!process.env.SIGNALK_DISABLE_SERVER_UPDATES) {
            (0, modules_1.checkForNewServerVersion)(app.config.version, serverUpgradeIsAvailable);
            app.intervals.push(setInterval(() => (0, modules_1.checkForNewServerVersion)(app.config.version, serverUpgradeIsAvailable), 60 * 1000 * 60 * 24));
        }
        this.app.providers = [];
        app.lastServerEvents = {};
        app.on('serverevent', (event) => {
            if (event.type) {
                app.lastServerEvents[event.type] = event;
            }
        });
        app.intervals.push((0, deltastats_1.startDeltaStatistics)(app));
        return new Promise(async (resolve, reject) => {
            createServer(app, async (err, server) => {
                if (err) {
                    reject(err);
                    return;
                }
                app.server = server;
                app.interfaces = {};
                app.clients = 0;
                debug('ID type: ' + app.selfType);
                debug('ID: ' + app.selfId);
                (0, config_1.sendBaseDeltas)(app);
                app.apis = await (0, api_1.startApis)(app);
                await startInterfaces(app);
                startMdns(app);
                app.pipedProviders = (0, pipedproviders_1.pipedProviders)(app);
                app.providers = app.pipedProviders.start();
                const primaryPort = (0, ports_1.getPrimaryPort)(app);
                debug(`primary port:${primaryPort}`);
                server.listen(primaryPort, () => {
                    console.log('signalk-server running at 0.0.0.0:' + primaryPort.toString() + '\n');
                    app.started = true;
                    resolve(self);
                });
                const secondaryPort = (0, ports_1.getSecondaryPort)(app);
                debug(`secondary port:${primaryPort}`);
                if (app.config.settings.ssl && secondaryPort) {
                    startRedirectToSsl(secondaryPort, (0, ports_1.getExternalPort)(app), (anErr, aServer) => {
                        if (!anErr) {
                            app.redirectServer = aServer;
                        }
                    });
                }
            });
        });
    }
    reload(mixed) {
        let settings;
        const self = this;
        if (typeof mixed === 'string') {
            try {
                settings = require(path_1.default.join(process.cwd(), mixed));
            }
            catch (_e) {
                debug(`Settings file '${settings}' does not exist`);
            }
        }
        if (mixed !== null && typeof mixed === 'object') {
            settings = mixed;
        }
        if (settings) {
            this.app.config.settings = settings;
        }
        this.stop().catch((e) => console.error(e));
        setTimeout(() => {
            self.start().catch((e) => console.error(e));
        }, 1000);
        return this;
    }
    async stop(cb) {
        if (!this.app.started) {
            return this;
        }
        try {
            lodash_1.default.each(this.app.interfaces, (intf) => {
                if (intf !== null &&
                    typeof intf === 'object' &&
                    typeof intf.stop === 'function') {
                    intf.stop();
                }
            });
            this.app.intervals.forEach((interval) => {
                clearInterval(interval);
            });
            this.app.providers.forEach((providerHolder) => {
                providerHolder.pipeElements[0].end();
            });
            debug('Closing server...');
            const that = this;
            return new Promise((resolve, reject) => {
                this.app.server.close(() => {
                    debug('Server closed');
                    if (that.app.redirectServer) {
                        try {
                            that.app.redirectServer.close(() => {
                                debug('Redirect server closed');
                                delete that.app.redirectServer;
                                that.app.started = false;
                                cb && cb();
                                resolve(that);
                            });
                        }
                        catch (err) {
                            reject(err);
                        }
                    }
                    else {
                        that.app.started = false;
                        cb && cb();
                        resolve(that);
                    }
                });
            });
        }
        catch (err) {
            throw err;
        }
    }
}
module.exports = Server;
function identifyPluginFromStack(stack, plugins) {
    for (const plugin of plugins) {
        if (stack.includes(plugin.packageName)) {
            return plugin.id;
        }
    }
    return undefined;
}
function installProcessErrorHandlers(app) {
    process.on('uncaughtException', (err) => {
        console.error('Uncaught exception:', err);
        if (app.plugins) {
            const pluginId = identifyPluginFromStack(err.stack ?? '', app.plugins);
            if (pluginId) {
                app.setPluginError(pluginId, `Uncaught error: ${err.message}`);
            }
        }
    });
    process.on('unhandledRejection', (reason) => {
        const err = reason instanceof Error ? reason : new Error(String(reason));
        console.error('Unhandled rejection:', err);
        if (app.plugins) {
            const pluginId = identifyPluginFromStack(err.stack ?? '', app.plugins);
            if (pluginId) {
                app.setPluginError(pluginId, `Unhandled rejection: ${err.message}`);
            }
        }
    });
}
function createServer(app, cb) {
    if (app.config.settings.ssl) {
        (0, security_js_1.getCertificateOptions)(app, (err, options) => {
            if (err) {
                cb(err);
            }
            else {
                debug('Starting server to serve both http and https');
                cb(null, https_1.default.createServer(options, app));
            }
        });
        return;
    }
    let server;
    try {
        debug('Starting server to serve only http');
        server = http_1.default.createServer(app);
    }
    catch (e) {
        cb(e);
        return;
    }
    cb(null, server);
}
function startRedirectToSsl(port, redirectPort, cb) {
    const redirectApp = (0, express_1.default)();
    redirectApp.use((req, res) => {
        const host = req.headers.host?.split(':')[0];
        res.redirect(`https://${host}:${redirectPort}${req.path}`);
    });
    const server = http_1.default.createServer(redirectApp);
    server.listen(port, () => {
        console.log(`Redirect server running on port ${port.toString()}`);
        cb(null, server);
    });
}
function startMdns(app) {
    if (lodash_1.default.isUndefined(app.config.settings.mdns) || app.config.settings.mdns) {
        debug(`Starting interface 'mDNS'`);
        try {
            app.interfaces.mdns = require('./mdns')(app);
        }
        catch (ex) {
            console.error('Could not start mDNS:' + ex);
        }
    }
    else {
        debug(`Interface 'mDNS' was disabled in configuration`);
    }
}
async function startInterfaces(app) {
    debug.enabled &&
        debug('Interfaces config:' + JSON.stringify(app.config.settings.interfaces));
    const argv = app.argv;
    const noPlugins = argv?.plugins === false;
    const noWebapps = argv?.webapps === false;
    const startupApp = app;
    if (noPlugins) {
        console.log('Plugins disabled by --no-plugins; skipping plugin loading');
        startupApp.plugins = [];
        startupApp.pluginsMap = {};
        startupApp.getPluginsList = async () => [];
    }
    if (noWebapps) {
        console.log('Webapps disabled by --no-webapps; skipping webapp loading');
        startupApp.webapps = [];
        startupApp.addons = [];
        startupApp.embeddablewebapps = [];
        startupApp.pluginconfigurators = [];
    }
    const skippedInterfaces = new Set();
    if (noPlugins) {
        skippedInterfaces.add('plugins').add('wasm');
    }
    if (noWebapps) {
        skippedInterfaces.add('webapps').add('mfd_webapp');
    }
    const availableInterfaces = require('./interfaces');
    return await Promise.all(Object.keys(availableInterfaces).map(async (name) => {
        if (skippedInterfaces.has(name)) {
            debug.enabled &&
                debug(`Not loading interface '${name}' (disabled by command line flag)`);
            return;
        }
        const theInterface = availableInterfaces[name];
        if (lodash_1.default.isUndefined(app.config.settings.interfaces) ||
            lodash_1.default.isUndefined((app.config.settings.interfaces || {})[name]) ||
            (app.config.settings.interfaces || {})[name]) {
            debug(`Loading interface '${name}'`);
            const boundEventMethods = app.wrappedEmitter.bindMethodsById(`interface:${name}`);
            const appCopy = {
                ...app,
                ...boundEventMethods
            };
            const handler = {
                set(obj, prop, value) {
                    ;
                    app[prop] = value;
                    return true;
                },
                get(target, prop, _receiver) {
                    return app[prop];
                }
            };
            const _interface = (appCopy.interfaces[name] = theInterface(new Proxy(appCopy, handler)));
            if (_interface && lodash_1.default.isFunction(_interface.start)) {
                if (lodash_1.default.isUndefined(_interface.forceInactive) ||
                    !_interface.forceInactive) {
                    debug(`Starting interface '${name}'`);
                    _interface.data = await _interface.start();
                }
                else {
                    debug(`Not starting interface '${name}' by forceInactive`);
                }
            }
        }
        else {
            debug(`Not loading interface '${name}' because of configuration`);
        }
    }));
}
function isValidUpdate(update) {
    if (update === null || typeof update !== 'object') {
        return false;
    }
    const values = update.values;
    if ('values' in update && !Array.isArray(values)) {
        return false;
    }
    if (Array.isArray(values) &&
        values.some((v) => v === null ||
            v === undefined ||
            typeof v.path !== 'string')) {
        return false;
    }
    return true;
}
//# sourceMappingURL=index.js.map