"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getNodeStatistics = exports.findNodesModifiedBefore = exports.fixNodeFilenames = exports.getNodeDataByMonitoringToken = exports.getNodeDataWithSecretsByMonitoringToken = exports.getNodeDataByToken = exports.getNodeDataWithSecretsByToken = exports.getNodeDataByMac = exports.getNodeDataWithSecretsByMac = exports.getAllNodes = exports.deleteNode = exports.internalUpdateNode = exports.updateNode = exports.createNode = void 0;
const lodash_1 = __importDefault(require("lodash"));
const async_1 = __importDefault(require("async"));
const crypto_1 = __importDefault(require("crypto"));
const graceful_fs_1 = __importStar(require("graceful-fs"));
const glob_1 = __importDefault(require("glob"));
const config_1 = require("../config");
const errorTypes_1 = __importDefault(require("../utils/errorTypes"));
const logger_1 = __importDefault(require("../logger"));
const MailService = __importStar(require("../services/mailService"));
const strings_1 = require("../utils/strings");
const urlBuilder_1 = require("../utils/urlBuilder");
const types_1 = require("../types");
const util_1 = __importDefault(require("util"));
const pglob = util_1.default.promisify(glob_1.default);
const linePrefixes = {
    hostname: '# Knotenname: ',
    nickname: '# Ansprechpartner: ',
    email: '# Kontakt: ',
    coords: '# Koordinaten: ',
    mac: '# MAC: ',
    token: '# Token: ',
    monitoring: '# Monitoring: ',
    monitoringToken: '# Monitoring-Token: '
};
const filenameParts = ['hostname', 'mac', 'key', 'token', 'monitoringToken'];
function generateToken() {
    return crypto_1.default.randomBytes(8).toString('hex');
}
function toNodeFilesPattern(filter) {
    const pattern = lodash_1.default.join(lodash_1.default.map(filenameParts, field => field in filter ? filter[field] : '*'), '@');
    return config_1.config.server.peersPath + '/' + pattern.toLowerCase();
}
function findNodeFiles(filter) {
    return pglob(toNodeFilesPattern(filter));
}
function findNodeFilesSync(filter) {
    return glob_1.default.sync(toNodeFilesPattern(filter));
}
async function findFilesInPeersPath() {
    const files = await pglob(config_1.config.server.peersPath + '/*');
    return await async_1.default.filter(files, (file, fileCallback) => {
        if (file[0] === '.') {
            return fileCallback(null, false);
        }
        graceful_fs_1.promises.lstat(file)
            .then(stats => fileCallback(null, stats.isFile()))
            .catch(fileCallback);
    });
}
function parseNodeFilename(filename) {
    const parts = lodash_1.default.split(filename, '@', filenameParts.length);
    const parsed = {};
    const zippedParts = lodash_1.default.zip(filenameParts, parts);
    lodash_1.default.each(zippedParts, part => {
        const key = part[0];
        if (key) {
            parsed[key] = part[1];
        }
    });
    return parsed;
}
function isDuplicate(filter, token) {
    const files = findNodeFilesSync(filter);
    if (files.length === 0) {
        return false;
    }
    if (files.length > 1 || !token /* node is being created*/) {
        return true;
    }
    return parseNodeFilename(files[0]).token !== token;
}
function checkNoDuplicates(token, node, nodeSecrets) {
    if (isDuplicate({ hostname: node.hostname }, token)) {
        throw { data: { msg: 'Already exists.', field: 'hostname' }, type: errorTypes_1.default.conflict };
    }
    if (node.key) {
        if (isDuplicate({ key: node.key }, token)) {
            throw { data: { msg: 'Already exists.', field: 'key' }, type: errorTypes_1.default.conflict };
        }
    }
    if (isDuplicate({ mac: node.mac }, token)) {
        throw { data: { msg: 'Already exists.', field: 'mac' }, type: errorTypes_1.default.conflict };
    }
    if (nodeSecrets.monitoringToken && isDuplicate({ monitoringToken: nodeSecrets.monitoringToken }, token)) {
        throw { data: { msg: 'Already exists.', field: 'monitoringToken' }, type: errorTypes_1.default.conflict };
    }
}
function toNodeFilename(token, node, nodeSecrets) {
    return config_1.config.server.peersPath + '/' +
        ((node.hostname || '') + '@' +
            (node.mac || '') + '@' +
            (node.key || '') + '@' +
            (token || '') + '@' +
            (nodeSecrets.monitoringToken || '')).toLowerCase();
}
async function writeNodeFile(isUpdate, token, node, nodeSecrets) {
    const filename = toNodeFilename(token, node, nodeSecrets);
    let data = '';
    lodash_1.default.each(linePrefixes, function (prefix, key) {
        let value;
        switch (key) {
            case 'monitoring':
                if (node.monitoring && node.monitoringConfirmed) {
                    value = 'aktiv';
                }
                else if (node.monitoring && !node.monitoringConfirmed) {
                    value = 'pending';
                }
                else {
                    value = '';
                }
                break;
            case 'monitoringToken':
                value = nodeSecrets.monitoringToken || '';
                break;
            default:
                value = key === 'token' ? token : node[key];
                if (lodash_1.default.isUndefined(value)) {
                    const nodeSecret = nodeSecrets[key];
                    value = lodash_1.default.isUndefined(nodeSecret) ? '' : nodeSecret;
                }
                break;
        }
        data += prefix + value + '\n';
    });
    if (node.key) {
        data += 'key "' + node.key + '";\n';
    }
    // since node.js is single threaded we don't need a lock
    if (isUpdate) {
        const files = findNodeFilesSync({ token: token });
        if (files.length !== 1) {
            throw { data: 'Node not found.', type: errorTypes_1.default.notFound };
        }
        checkNoDuplicates(token, node, nodeSecrets);
        const file = files[0];
        try {
            graceful_fs_1.default.unlinkSync(file);
        }
        catch (error) {
            logger_1.default.tag('node', 'save').error('Could not delete old node file: ' + file, error);
            throw { data: 'Could not remove old node data.', type: errorTypes_1.default.internalError };
        }
    }
    else {
        checkNoDuplicates(null, node, nodeSecrets);
    }
    try {
        graceful_fs_1.default.writeFileSync(filename, data, 'utf8');
        return { token, node };
    }
    catch (error) {
        logger_1.default.tag('node', 'save').error('Could not write node file: ' + filename, error);
        throw { data: 'Could not write node data.', type: errorTypes_1.default.internalError };
    }
}
async function deleteNodeFile(token) {
    let files;
    try {
        files = await findNodeFiles({ token: token });
    }
    catch (error) {
        logger_1.default.tag('node', 'delete').error('Could not find node file: ' + files, error);
        throw { data: 'Could not delete node.', type: errorTypes_1.default.internalError };
    }
    if (files.length !== 1) {
        throw { data: 'Node not found.', type: errorTypes_1.default.notFound };
    }
    try {
        graceful_fs_1.default.unlinkSync(files[0]);
    }
    catch (error) {
        logger_1.default.tag('node', 'delete').error('Could not delete node file: ' + files, error);
        throw { data: 'Could not delete node.', type: errorTypes_1.default.internalError };
    }
}
async function parseNodeFile(file) {
    const contents = await graceful_fs_1.promises.readFile(file);
    const stats = await graceful_fs_1.promises.lstat(file);
    const modifiedAt = Math.floor(stats.mtimeMs / 1000);
    const lines = contents.toString();
    const node = {};
    const nodeSecrets = {};
    lodash_1.default.each(lines.split('\n'), function (line) {
        const entries = {};
        for (const key of Object.keys(linePrefixes)) {
            const prefix = linePrefixes[key];
            if (line.substring(0, prefix.length) === prefix) {
                entries[key] = strings_1.normalizeString(line.substr(prefix.length));
                break;
            }
        }
        if (lodash_1.default.isEmpty(entries) && line.substring(0, 5) === 'key "') {
            entries.key = strings_1.normalizeString(line.split('"')[1]);
        }
        lodash_1.default.each(entries, function (value, key) {
            switch (key) {
                case 'mac':
                    node.mac = value;
                    node.mapId = lodash_1.default.toLower(value).replace(/:/g, '');
                    break;
                case 'monitoring':
                    const active = value === 'aktiv';
                    const pending = value === 'pending';
                    node.monitoring = active || pending;
                    node.monitoringConfirmed = active;
                    node.monitoringState =
                        active ? types_1.MonitoringState.ACTIVE : (pending ? types_1.MonitoringState.PENDING : types_1.MonitoringState.DISABLED);
                    break;
                case 'monitoringToken':
                    nodeSecrets.monitoringToken = value;
                    break;
                default:
                    node[key] = value;
                    break;
            }
        });
    });
    return {
        node: {
            token: node.token || '',
            nickname: node.nickname || '',
            email: node.email || '',
            hostname: node.hostname || '',
            coords: node.coords || undefined,
            key: node.key || undefined,
            mac: node.mac || '',
            monitoring: !!node.monitoring,
            monitoringConfirmed: !!node.monitoringConfirmed,
            monitoringState: node.monitoringState || types_1.MonitoringState.DISABLED,
            modifiedAt,
        },
        nodeSecrets: {
            monitoringToken: nodeSecrets.monitoringToken || undefined,
        },
    };
}
async function findNodeDataByFilePattern(filter) {
    const files = await findNodeFiles(filter);
    if (files.length !== 1) {
        return null;
    }
    const file = files[0];
    return await parseNodeFile(file);
}
async function getNodeDataByFilePattern(filter) {
    const result = await findNodeDataByFilePattern(filter);
    if (!result) {
        throw { data: 'Node not found.', type: errorTypes_1.default.notFound };
    }
    return result;
}
async function sendMonitoringConfirmationMail(node, nodeSecrets) {
    const monitoringToken = nodeSecrets.monitoringToken;
    if (!monitoringToken) {
        logger_1.default
            .tag('monitoring', 'confirmation')
            .error('Could not enqueue confirmation mail. No monitoring token found.');
        throw { data: 'Internal error.', type: errorTypes_1.default.internalError };
    }
    const confirmUrl = urlBuilder_1.monitoringConfirmUrl(monitoringToken);
    const disableUrl = urlBuilder_1.monitoringDisableUrl(monitoringToken);
    await MailService.enqueue(config_1.config.server.email.from, node.nickname + ' <' + node.email + '>', 'monitoring-confirmation', {
        node: node,
        confirmUrl: confirmUrl,
        disableUrl: disableUrl
    });
}
async function createNode(node) {
    const token = generateToken();
    const nodeSecrets = {};
    node.monitoringConfirmed = false;
    if (node.monitoring) {
        nodeSecrets.monitoringToken = generateToken();
    }
    const written = await writeNodeFile(false, token, node, nodeSecrets);
    if (written.node.monitoring && !written.node.monitoringConfirmed) {
        await sendMonitoringConfirmationMail(written.node, nodeSecrets);
    }
    return written;
}
exports.createNode = createNode;
async function updateNode(token, node) {
    const { node: currentNode, nodeSecrets } = await getNodeDataWithSecretsByToken(token);
    let monitoringConfirmed = false;
    let monitoringToken = '';
    if (node.monitoring) {
        if (!currentNode.monitoring) {
            // monitoring just has been enabled
            monitoringConfirmed = false;
            monitoringToken = generateToken();
        }
        else {
            // monitoring is still enabled
            if (currentNode.email !== node.email) {
                // new email so we need a new token and a reconfirmation
                monitoringConfirmed = false;
                monitoringToken = generateToken();
            }
            else {
                // email unchanged, keep token (fix if not set) and confirmation state
                monitoringConfirmed = currentNode.monitoringConfirmed;
                monitoringToken = nodeSecrets.monitoringToken || generateToken();
            }
        }
    }
    node.monitoringConfirmed = monitoringConfirmed;
    nodeSecrets.monitoringToken = monitoringToken;
    const written = await writeNodeFile(true, token, node, nodeSecrets);
    if (written.node.monitoring && !written.node.monitoringConfirmed) {
        await sendMonitoringConfirmationMail(written.node, nodeSecrets);
    }
    return written;
}
exports.updateNode = updateNode;
async function internalUpdateNode(token, node, nodeSecrets) {
    return await writeNodeFile(true, token, node, nodeSecrets);
}
exports.internalUpdateNode = internalUpdateNode;
async function deleteNode(token) {
    await deleteNodeFile(token);
}
exports.deleteNode = deleteNode;
async function getAllNodes() {
    let files;
    try {
        files = await findNodeFiles({});
    }
    catch (error) {
        logger_1.default.tag('nodes').error('Error getting all nodes:', error);
        throw { data: 'Internal error.', type: errorTypes_1.default.internalError };
    }
    const nodes = [];
    for (const file of files) {
        try {
            const { node } = await parseNodeFile(file);
            nodes.push(node);
        }
        catch (error) {
            logger_1.default.tag('nodes').error('Error getting all nodes:', error);
            throw { data: 'Internal error.', type: errorTypes_1.default.internalError };
        }
    }
    return nodes;
}
exports.getAllNodes = getAllNodes;
async function getNodeDataWithSecretsByMac(mac) {
    return await findNodeDataByFilePattern({ mac: mac });
}
exports.getNodeDataWithSecretsByMac = getNodeDataWithSecretsByMac;
async function getNodeDataByMac(mac) {
    const result = await findNodeDataByFilePattern({ mac: mac });
    return result ? result.node : null;
}
exports.getNodeDataByMac = getNodeDataByMac;
async function getNodeDataWithSecretsByToken(token) {
    return await getNodeDataByFilePattern({ token: token });
}
exports.getNodeDataWithSecretsByToken = getNodeDataWithSecretsByToken;
async function getNodeDataByToken(token) {
    const { node } = await getNodeDataByFilePattern({ token: token });
    return node;
}
exports.getNodeDataByToken = getNodeDataByToken;
async function getNodeDataWithSecretsByMonitoringToken(monitoringToken) {
    return await getNodeDataByFilePattern({ monitoringToken: monitoringToken });
}
exports.getNodeDataWithSecretsByMonitoringToken = getNodeDataWithSecretsByMonitoringToken;
async function getNodeDataByMonitoringToken(monitoringToken) {
    const { node } = await getNodeDataByFilePattern({ monitoringToken: monitoringToken });
    return node;
}
exports.getNodeDataByMonitoringToken = getNodeDataByMonitoringToken;
async function fixNodeFilenames() {
    const files = await findFilesInPeersPath();
    for (const file of files) {
        const { node, nodeSecrets } = await parseNodeFile(file);
        const expectedFilename = toNodeFilename(node.token, node, nodeSecrets);
        if (file !== expectedFilename) {
            try {
                await graceful_fs_1.promises.rename(file, expectedFilename);
            }
            catch (error) {
                throw new Error('Cannot rename file ' + file + ' to ' + expectedFilename + ' => ' + error);
            }
        }
    }
}
exports.fixNodeFilenames = fixNodeFilenames;
async function findNodesModifiedBefore(timestamp) {
    const nodes = await getAllNodes();
    return lodash_1.default.filter(nodes, node => node.modifiedAt < timestamp);
}
exports.findNodesModifiedBefore = findNodesModifiedBefore;
async function getNodeStatistics() {
    const nodes = await getAllNodes();
    const nodeStatistics = {
        registered: lodash_1.default.size(nodes),
        withVPN: 0,
        withCoords: 0,
        monitoring: {
            active: 0,
            pending: 0
        }
    };
    lodash_1.default.each(nodes, function (node) {
        if (node.key) {
            nodeStatistics.withVPN += 1;
        }
        if (node.coords) {
            nodeStatistics.withCoords += 1;
        }
        function ensureExhaustive(monitoringState) {
            throw new Error('Add missing case for monitoring stat below: ' + monitoringState);
        }
        const monitoringState = node.monitoringState;
        switch (monitoringState) {
            case types_1.MonitoringState.ACTIVE:
                nodeStatistics.monitoring.active += 1;
                break;
            case types_1.MonitoringState.PENDING:
                nodeStatistics.monitoring.pending += 1;
                break;
            case types_1.MonitoringState.DISABLED:
                // Not counted seperately.
                break;
            default:
                ensureExhaustive(monitoringState);
        }
    });
    return nodeStatistics;
}
exports.getNodeStatistics = getNodeStatistics;
//# sourceMappingURL=nodeService.js.map