"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (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.getLastNTPObject = exports.getFakeTimeOffset = exports.clearFakeTimeOffset = exports.calculateFakeTimeOffset = exports.getNetworkTimeOffset = exports.shardusGetTime = exports.checkAndUpdateTimeSyncedOffset = exports.init = exports.NetworkClass = exports.ipInfo = void 0;
const sntp_1 = __importDefault(require("@hapi/sntp"));
const lib_net_1 = require("@shardeum-foundation/lib-net");
const body_parser_1 = __importDefault(require("body-parser"));
const cors_1 = __importDefault(require("cors"));
const events_1 = require("events");
const express_1 = __importDefault(require("express"));
const net = __importStar(require("net"));
const util_1 = require("util");
const debug_1 = require("../debug");
const httpModule = __importStar(require("../http"));
const logger_1 = require("../logger");
const Context_1 = require("../p2p/Context");
const Utils_1 = require("../p2p/Utils");
const utils = __importStar(require("../utils"));
const utils_1 = require("../utils");
const nestedCounters_1 = require("../utils/nestedCounters");
const profiler_1 = require("../utils/profiler");
const NatAPI = require("nat-api");
const lib_types_1 = require("@shardeum-foundation/lib-types");
/** STATE */
let mainLogger;
let natClient;
let ntpOffsetMs = 0;
let fakeTimeOffsetMs = 0;
let lastNTPTimeObj = {};
/** CLASS */
class NetworkClass extends events_1.EventEmitter {
    constructor(config, logger) {
        super();
        this.app = (0, express_1.default)();
        this.sn = null;
        this.logger = logger;
        this.mainLogger = logger.getLogger('main');
        this.netLogger = logger.getLogger('net');
        this.timeout = config.network.timeout * 1000;
        this.internalRoutes = {};
        this.externalRoutes = [];
        this.extServer = null;
        this.intServer = null;
        this.InternalTellCounter = 1;
        this.InternalAskCounter = 1;
        this.debugNetworkDelay = 0;
        this.statisticsInstance = null;
        ntpOffsetMs = 0;
        fakeTimeOffsetMs = 0;
        if (config && config.debug && config.debug.fakeNetworkDelay) {
            this.debugNetworkDelay = config.debug.fakeNetworkDelay;
        }
        nestedCounters_1.nestedCountersInstance.countEvent('network', 'init');
        this.useLruCacheForSocketMgmt = config.p2p.useLruCacheForSocketMgmt;
        this.lruCacheSizeForSocketMgmt = config.p2p.lruCacheSizeForSocketMgmt;
        this.shardusCryptoHashKey = config.crypto.hashKey;
        this.payloadSizeLimitInBytes = config.p2p.payloadSizeLimitInBytes;
        this.headerSizeLimitInBytes = config.p2p.headerSizeLimitInBytes;
    }
    setDebugNetworkDelay(delay) {
        this.debugNetworkDelay = delay;
    }
    setStatisticsInstance(statistics) {
        this.statisticsInstance = statistics;
    }
    customSendJsonMiddleware(req, res, next) {
        const originalSend = res.send;
        res.send = function (data) {
            if (typeof data === 'object' && data !== null) {
                const jsonString = lib_types_1.Utils.safeStringify(data);
                res.setHeader('Content-Type', 'application/json');
                return originalSend.call(this, jsonString);
            }
            return originalSend.call(this, data);
        };
        res.json = function (data) {
            const jsonString = lib_types_1.Utils.safeStringify(data);
            res.setHeader('Content-Type', 'application/json');
            return originalSend.call(this, jsonString);
        };
        next();
    }
    // TODO: Allow for binding to a specified network interface
    _setupExternal() {
        return new Promise((resolve, reject) => {
            const self = this;
            const storeRequests = function (req, res, next) {
                if (req.url !== '/test') {
                    if (self.verboseLogsNet) {
                        self.netLogger.debug('External\t' +
                            lib_types_1.Utils.safeStringify({
                                url: req.url,
                                method: req.method,
                                body: req.body,
                            }));
                    }
                }
                next();
            };
            this.app.use(body_parser_1.default.json({ limit: '50mb', reviver: lib_types_1.Utils.typeReviver }));
            this.app.use(body_parser_1.default.urlencoded({ limit: '50mb', extended: true }));
            this.app.use((0, cors_1.default)());
            this.app.use(this.customSendJsonMiddleware);
            this.app.use(storeRequests);
            this._applyExternal();
            this.app.use((err, req, res, next) => {
                res.status(500).json({
                    error: 'Internal Server Error',
                    message: (0, debug_1.isDebugMode)() ? err.message : 'An unexpected error occurred',
                });
            });
            this.extServer = this.app.listen(this.ipInfo.externalPort, () => {
                const msg = `External server running on port ${this.ipInfo.externalPort}...`;
                console.log(msg);
                this.mainLogger.info('Network: ' + msg);
            });
            this.extServer.setTimeout(Context_1.config.network.timeout * 1000);
            this.io = require('socket.io')(this.extServer);
            resolve(this.io);
        });
    }
    // TODO: Allow for binding to a specified network interface
    async _setupInternal() {
        this.sn = (0, lib_net_1.Sn)({
            port: this.ipInfo.internalPort,
            senderOpts: {
                useLruCache: this.useLruCacheForSocketMgmt,
                lruSize: this.lruCacheSizeForSocketMgmt,
            },
            headerOpts: {
                sendHeaderVersion: 1,
            },
            customStringifier: lib_types_1.Utils.safeStringify,
            customJsonParser: lib_types_1.Utils.safeJsonParse,
            crypto: {
                hashKey: this.shardusCryptoHashKey,
                signingSecretKeyHex: this.signingSecretKeyHex,
            },
            payloadOpts: {
                payloadSizeLimitInBytes: this.payloadSizeLimitInBytes,
                headerSizeLimitInBytes: this.headerSizeLimitInBytes,
            },
        });
        this.intServer = await this.sn.listen(async (data, remote, respond, header, sign) => {
            let routeName;
            try {
                if (!data)
                    throw new Error('No data provided in request...');
                const { route, payload } = data;
                routeName = route;
                if (!route && payload) {
                    if (logger_1.logFlags.debug)
                        this.mainLogger.debug('Received response data without any specified route', payload);
                    return;
                }
                if (!route && data.error) {
                    if (logger_1.logFlags.debug)
                        this.mainLogger.debug('Received a hang prevention dummy payload');
                    return;
                }
                if (!route) {
                    /* prettier-ignore */ if (logger_1.logFlags.debug)
                        this.mainLogger.debug('Network: ' + `Unable to read request, payload of received message: ${lib_types_1.Utils.safeStringify(data)}`);
                    throw new Error('Unable to read request, no route specified.');
                }
                if (!this.internalRoutes[route])
                    throw new Error('Unable to handle request, invalid route.');
                if (this.debugNetworkDelay > 0) {
                    await utils.sleep(this.debugNetworkDelay);
                }
                // Check if we should respond slowly
                if (Context_1.config?.debug?.slowResponseChance > 0 && Math.random() < Context_1.config.debug.slowResponseChance) {
                    const delayMs = Context_1.config.debug.slowResponseDelay || 3000;
                    nestedCounters_1.nestedCountersInstance.countEvent('network', 'slow-response-injected');
                    /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                        mainLogger.info(`Injecting slow response delay of ${delayMs}ms for route: ${route}`);
                    await utils.sleep(delayMs);
                }
                profiler_1.profilerInstance.profileSectionStart('net-internl');
                profiler_1.profilerInstance.profileSectionStart(`net-internl-${route}`);
                const handler = this.internalRoutes[route];
                if (!payload) {
                    await handler(null, respond, header, sign);
                    return;
                }
                await handler(payload, respond, header, sign);
                if (logger_1.logFlags.net_trace) {
                    this.netLogger.debug('Internal\t' +
                        lib_types_1.Utils.safeStringify({
                            url: route,
                            body: payload,
                        }));
                }
            }
            catch (err) {
                if (logger_1.logFlags.error)
                    this.mainLogger.error('Network: _setupInternal: ', err);
                if (logger_1.logFlags.error)
                    this.mainLogger.error('DBG', 'Network: _setupInternal > sn.listen > callback > data', data);
                if (logger_1.logFlags.error)
                    this.mainLogger.error('DBG', 'Network: _setupInternal > sn.listen > callback > remote', remote);
            }
            finally {
                profiler_1.profilerInstance.profileSectionEnd('net-internl');
                profiler_1.profilerInstance.profileSectionEnd(`net-internl-${routeName}`);
            }
        });
        console.log(`Internal server running on port ${this.ipInfo.internalPort}...`);
        //pass logflags to shardus network
        this.sn.setLogFlags(logger_1.logFlags);
    }
    /**
     *
     * @param nodes
     * @param route
     * @param message
     * @param alreadyLogged this is so that gossip system can indicate that we already have recorded playback logs
     */
    async tell(nodes, route, message, alreadyLogged = false, subRoute = '') {
        const data = { route, payload: message };
        const promises = [];
        let id = '';
        if (message.tracker) {
            id = message.tracker;
        }
        if (!nodes || nodes.length == 0) {
            /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                console.log("returning tell because the node list is empty for route:", route);
            return;
        }
        for (const node of nodes) {
            /* prettier-ignore */ if (logger_1.logFlags.playback && alreadyLogged === false)
                this.logger.playbackLog('self', node, 'InternalTell', route, id, message);
            const requestId = (0, Utils_1.generateUUID)();
            /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                mainLogger.info(`Initiating tell request with requestId: ${requestId}`);
            /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                mainLogger.info(`requestId: ${requestId}, node: ${utils.logNode(node)}`);
            /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                mainLogger.info(`route: ${route} ${subRoute}, message: ${message} requestId: ${requestId}`);
            this.InternalTellCounter++;
            // Check if we should drop this message
            if (Context_1.config?.debug?.dropMessageChance > 0 && Math.random() < Context_1.config.debug.dropMessageChance) {
                nestedCounters_1.nestedCountersInstance.countEvent('network', 'message-dropped-tell');
                /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                    mainLogger.info(`Dropping tell message due to dropMessageChance: ${route} ${subRoute}`);
                continue;
            }
            const promise = this.sn.send(node.internalPort, node.internalIp, data);
            promise.catch((err) => {
                /* prettier-ignore */ if (logger_1.logFlags.error)
                    this.mainLogger.error(`Network error (tell) on ${route} ${subRoute}: ${(0, utils_1.formatErrorMessage)(err)}`);
                let errorGroup = ('' + err).slice(0, 20);
                nestedCounters_1.nestedCountersInstance.countEvent('network', `error2-tell ${route} ${subRoute}`);
                this.emit('error', node, requestId, 'tell', errorGroup, route, subRoute);
            });
            promises.push(promise);
        }
        try {
            await Promise.all(promises);
        }
        catch (err) {
            nestedCounters_1.nestedCountersInstance.countEvent('network', `error-tell ${route} ${subRoute}`);
            /* prettier-ignore */ if (logger_1.logFlags.error)
                this.mainLogger.error(`Network error (tell-err) on ${route} ${subRoute}: ${(0, utils_1.formatErrorMessage)(err)}`);
        }
    }
    async tellBinary(nodes, route, message, appHeader, trackerId, alreadyLogged = false) {
        const data = { route, payload: message };
        const promises = [];
        if (!nodes || nodes.length == 0) {
            /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                console.log("returning from tellBinary because the node list is empty for route:", route);
            return;
        }
        if (Context_1.config.p2p.useCombinedTellBinary) {
            const ports = [];
            const addresses = [];
            const requestId = (0, Utils_1.generateUUID)();
            for (const node of nodes) {
                /* prettier-ignore */ if (logger_1.logFlags.playback && alreadyLogged === false)
                    this.logger.playbackLog('self', node, 'InternalTellBinary', route, trackerId, message);
                /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                    this.mainLogger.info(`tellBinary: initiating tell request with useCombinedTellBinary enabled on requestId: ${requestId}`);
                /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                    this.mainLogger.info(`tellBinary: requestId: ${requestId}, node: ${utils.logNode(node)}`);
                /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                    this.mainLogger.info(`tellBinary: route: ${route}, message: ${message} requestId: ${requestId}`);
                this.InternalTellCounter++;
                ports.push(node.internalPort);
                addresses.push(node.internalIp);
            }
            try {
                await this.sn.multiSendWithHeader(ports, addresses, data, appHeader);
            }
            catch (err) {
                let errorGroup = ('' + err).slice(0, 20);
                nestedCounters_1.nestedCountersInstance.countEvent('network', `error2-tellBinary ${route}`);
                //commenting out to temp fix GOLD-46 and GOLD-52
                // this.emit('error', nodes, requestId, 'tellBinary', errorGroup, route)
                /* prettier-ignore */ if (logger_1.logFlags.error)
                    this.mainLogger.error(`Network error (tellBinary) on ${route}: ${(0, utils_1.formatErrorMessage)(err)}`);
            }
        }
        else {
            for (const node of nodes) {
                /* prettier-ignore */ if (logger_1.logFlags.playback && alreadyLogged === false)
                    this.logger.playbackLog('self', node, 'InternalTellBinary', route, trackerId, message);
                const requestId = (0, Utils_1.generateUUID)();
                /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                    this.mainLogger.info(`tellBinary: initiating tell request with useCombinedTellBinary disabled on requestId: ${requestId}`);
                /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                    this.mainLogger.info(`tellBinary: requestId: ${requestId}, node: ${utils.logNode(node)}`);
                /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                    this.mainLogger.info(`tellBinary: route: ${route}, message: ${message} requestId: ${requestId}`);
                this.InternalTellCounter++;
                // Check if we should drop this message
                if (Context_1.config?.debug?.dropMessageChance > 0 && Math.random() < Context_1.config.debug.dropMessageChance) {
                    nestedCounters_1.nestedCountersInstance.countEvent('network', 'message-dropped-tellBinary');
                    /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                        mainLogger.info(`Dropping tellBinary message due to dropMessageChance: ${route}`);
                    continue;
                }
                const promise = this.sn.sendWithHeader(node.internalPort, node.internalIp, data, appHeader);
                promise.catch((err) => {
                    /* prettier-ignore */ if (logger_1.logFlags.error)
                        this.mainLogger.error(`Network error (tellBinary) on ${route}: ${(0, utils_1.formatErrorMessage)(err)}`);
                    let errorGroup = ('' + err).slice(0, 20);
                    nestedCounters_1.nestedCountersInstance.countEvent('network', `error2-tellBinary ${route}`);
                    this.emit('error', node, requestId, 'tellBinary', errorGroup, route);
                });
                promises.push(promise);
            }
            try {
                await Promise.all(promises);
            }
            catch (err) {
                nestedCounters_1.nestedCountersInstance.countEvent('network', `error-tellBinary ${route}`);
                /* prettier-ignore */ if (logger_1.logFlags.error)
                    this.mainLogger.error(`Network error (tellBinary-promise) on ${route}: ${(0, utils_1.formatErrorMessage)(err)}`);
            }
        }
    }
    ask(node, route, message, alreadyLogged = false, extraTime = 0) {
        return new Promise(async (resolve, reject) => {
            this.InternalAskCounter++;
            let id = '';
            if (message.tracker) {
                id = message.tracker;
            }
            const requestId = (0, Utils_1.generateUUID)();
            /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                mainLogger.info(`Initiating ask request with requestId: ${requestId}`);
            /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                mainLogger.info(`requestId: ${requestId}, node: ${utils.logNode(node)}`);
            /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                mainLogger.info(`route: ${route}, message: ${message} requestId: ${requestId}`);
            try {
                if (this.debugNetworkDelay > 0) {
                    await utils.sleep(this.debugNetworkDelay);
                }
                profiler_1.profilerInstance.profileSectionStart('net-ask');
                profiler_1.profilerInstance.profileSectionStart(`net-ask-${route}`);
                const data = { route, payload: message };
                const onRes = (res) => {
                    /* prettier-ignore */ if (logger_1.logFlags.playback && alreadyLogged === false)
                        this.logger.playbackLog('self', node, 'InternalAskResp', route, id, res);
                    resolve(res);
                };
                const onTimeout = () => {
                    nestedCounters_1.nestedCountersInstance.countEvent('network', 'timeout');
                    if (this.statisticsInstance)
                        this.statisticsInstance.incrementCounter('networkTimeout');
                    const err = new Error(`Request timed out. ${utils.stringifyReduce(id)}`);
                    nestedCounters_1.nestedCountersInstance.countRareEvent('network', 'timeout ' + route);
                    /* prettier-ignore */ if (logger_1.logFlags.error)
                        this.mainLogger.error(`Network timeout (ask) on ${route}: ${(0, utils_1.formatErrorMessage)(err)}`);
                    this.emit('timeout', node, requestId, 'ask');
                    reject(err);
                };
                /* prettier-ignore */ if (logger_1.logFlags.playback && alreadyLogged === false)
                    this.logger.playbackLog('self', node, 'InternalAsk', route, id, message);
                // Check if we should drop this message
                if (Context_1.config?.debug?.dropMessageChance > 0 && Math.random() < Context_1.config.debug.dropMessageChance) {
                    nestedCounters_1.nestedCountersInstance.countEvent('network', 'message-dropped-ask');
                    /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                        mainLogger.info(`Dropping ask message due to dropMessageChance: ${route}`);
                    reject(new Error('Message dropped due to dropMessageChance'));
                    return;
                }
                try {
                    await this.sn.send(node.internalPort, node.internalIp, data, this.timeout + extraTime, onRes, onTimeout);
                }
                catch (err) {
                    nestedCounters_1.nestedCountersInstance.countEvent('network', `error-ask ${route}`);
                    /* prettier-ignore */ if (logger_1.logFlags.error)
                        this.mainLogger.error(`Network error (ask-err) on ${route}: ${(0, utils_1.formatErrorMessage)(err)}`);
                    let errorGroup = ('' + err).slice(0, 20);
                    this.emit('error', node, requestId, 'ask', errorGroup, route, '');
                }
            }
            finally {
                profiler_1.profilerInstance.profileSectionEnd('net-ask');
                profiler_1.profilerInstance.profileSectionEnd(`net-ask-${route}`);
            }
        });
    }
    askBinary(node, route, message, appHeader, trackerId, alreadyLogged = false, extraTime = 0) {
        return new Promise(async (resolve, reject) => {
            this.InternalAskCounter++;
            const requestId = (0, Utils_1.generateUUID)();
            /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                this.mainLogger.info(`askBinary: initiating ask request with requestId: ${requestId}`);
            /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                this.mainLogger.info(`askBinary: requestId: ${requestId}, node: ${utils.logNode(node)}`);
            /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                this.mainLogger.info(`askBinary: route: ${route}, message: ${message} requestId: ${requestId}`);
            try {
                if (this.debugNetworkDelay > 0) {
                    await utils.sleep(this.debugNetworkDelay);
                }
                profiler_1.profilerInstance.profileSectionStart('net-askBinary');
                profiler_1.profilerInstance.profileSectionStart(`net-askBinary-${route}`);
                const data = { route, payload: message };
                const onRes = (res, header, sign) => {
                    /* prettier-ignore */ if (logger_1.logFlags.playback && alreadyLogged === false)
                        this.logger.playbackLog('self', node, 'InternalAskResp', route, trackerId, res);
                    resolve({ res, header, sign });
                };
                const onTimeout = () => {
                    nestedCounters_1.nestedCountersInstance.countEvent('network', 'timeout');
                    if (this.statisticsInstance)
                        this.statisticsInstance.incrementCounter('networkTimeout');
                    const err = new Error(`askBinary: request timed out. ${utils.stringifyReduce(trackerId)}`);
                    nestedCounters_1.nestedCountersInstance.countRareEvent('network', 'timeout ' + route);
                    /* prettier-ignore */ if (logger_1.logFlags.error)
                        this.mainLogger.error(`Network timeout (askBinary) on ${route}: ${(0, utils_1.formatErrorMessage)(err)} node: ${utils.logNode(node)}`);
                    this.emit('timeout', node, requestId, 'askBinary');
                    reject(err);
                };
                /* prettier-ignore */ if (logger_1.logFlags.playback && alreadyLogged === false)
                    this.logger.playbackLog('self', node, 'InternalAsk', route, trackerId, message);
                // Check if we should drop this message
                if (Context_1.config?.debug?.dropMessageChance > 0 && Math.random() < Context_1.config.debug.dropMessageChance) {
                    nestedCounters_1.nestedCountersInstance.countEvent('network', 'message-dropped-askBinary');
                    /* prettier-ignore */ if (logger_1.logFlags.net_verbose)
                        mainLogger.info(`Dropping askBinary message due to dropMessageChance: ${route}`);
                    reject(new Error('Message dropped due to dropMessageChance'));
                    return;
                }
                try {
                    await this.sn.sendWithHeader(node.internalPort, node.internalIp, data, appHeader, this.timeout + extraTime, onRes, onTimeout);
                }
                catch (err) {
                    nestedCounters_1.nestedCountersInstance.countEvent('network', `error-askBinary ${route}`);
                    /* prettier-ignore */ if (logger_1.logFlags.error)
                        this.mainLogger.error(`Network error (askBinary) on ${route}: ${(0, utils_1.formatErrorMessage)(err)}`);
                    let errorGroup = ('' + err).slice(0, 20);
                    //this.mainLogger.info(`askBinary: sendWithHeader: error: ${err}`)
                    this.emit('error', node, requestId, 'ask', errorGroup, route, '');
                }
            }
            finally {
                profiler_1.profilerInstance.profileSectionEnd('net-askBinary');
                profiler_1.profilerInstance.profileSectionEnd(`net-askBinary-${route}`);
            }
        });
    }
    evictCachedSockets(nodes) {
        if (!this.sn)
            return;
        for (const node of nodes) {
            try {
                profiler_1.profilerInstance.profileSectionStart('net-evictCachedSockets');
                /* prettier-ignore */ if (logger_1.logFlags.debug)
                    this.mainLogger.debug(`Evicting socket for node ${node.id}, (ip: ${node.internalIp}, port: ${node.internalPort})`);
                this.sn.evictSocket(node.internalPort, node.internalIp);
                nestedCounters_1.nestedCountersInstance.countEvent('network', 'evict-cached-sockets');
            }
            catch (err) {
                /* prettier-ignore */ if (logger_1.logFlags.error)
                    this.mainLogger.error(`Error evicting socket for node ${node.id}: ${err}, (ip: ${node.internalIp}, port: ${node.internalPort})`);
            }
            finally {
                /* prettier-ignore */ if (logger_1.logFlags.debug)
                    this.mainLogger.debug(`Evicted socket for node ${node.id}, (ip: ${node.internalIp}, port: ${node.internalPort})`);
                profiler_1.profilerInstance.profileSectionEnd('net-evictCachedSockets');
            }
        }
    }
    async setup(ipInfo, signingSecretKeyHex) {
        if (!ipInfo.externalIp)
            throw new Error('Fatal: network module requires externalIp');
        if (!ipInfo.externalPort)
            throw new Error('Fatal: network module requires externalPort');
        if (!ipInfo.internalIp)
            throw new Error('Fatal: network module requires internalIp');
        if (!ipInfo.internalPort)
            throw new Error('Fatal: network module requires internalPort');
        this.ipInfo = ipInfo;
        this.signingSecretKeyHex = signingSecretKeyHex;
        this.logger.setPlaybackIPInfo(ipInfo);
        this._setupInternal();
        return await this._setupExternal();
    }
    async shutdown() {
        try {
            const promises = [];
            if (this.extServer)
                promises.push(closeServer(this.extServer));
            // [TODO] - need to see why it is taking minutes for stopListening promises to return; for now Omar decided to comment this out
            //      if (this.sn) promises.push(this.sn.stopListening(this.intServer))
            if (natClient)
                promises.push(natClient.es6.destroy());
            await Promise.all(promises);
        }
        catch (e) {
            if (e.code !== 'ERR_SERVER_NOT_RUNNING')
                throw e;
        }
    }
    _registerExternal(method, route, authHandler, responseHandler) {
        const formattedRoute = `/${route}`;
        const handlers = [];
        // This logic normalizes the optional parameter of the method signature.
        // If the responseHandler is null, then the value set as the authHandler param is actually the responseHandler.
        if (!responseHandler) {
            responseHandler = authHandler;
            authHandler = null;
        }
        if (logger_1.logFlags.playback) {
            const playbackHandler = (req, res, next) => {
                this.logger.playbackLog(req.hostname, 'self', 'ExternalHttpReq', formattedRoute, '', {
                    params: req.params,
                    body: req.body,
                });
                next();
            };
            handlers.push(playbackHandler);
        }
        if (authHandler) {
            handlers.push(authHandler);
        }
        const wrappedHandler = async (req, res, next) => {
            let result;
            try {
                if ((0, debug_1.isDebugMode)() && ['GET', 'POST'].includes(method)) {
                    profiler_1.profilerInstance.profileSectionStart('net-externl', false);
                    profiler_1.profilerInstance.profileSectionStart(`net-externl-${route}`, false);
                    profiler_1.profilerInstance.scopedProfileSectionStart(`net-externl-${route}`, false);
                }
                result = await responseHandler(req, res, next);
            }
            catch (error) {
                /* prettier-ignore */ if (logger_1.logFlags.error)
                    this.mainLogger.error(`Error in route ${route}: ${error.message}`);
                nestedCounters_1.nestedCountersInstance.countEvent('endpoint-exception', `${route}`);
                next(error);
            }
            finally {
                if ((0, debug_1.isDebugMode)() && ['GET', 'POST'].includes(method)) {
                    profiler_1.profilerInstance.scopedProfileSectionEnd(`net-externl-${route}`);
                    profiler_1.profilerInstance.profileSectionEnd(`net-externl-${route}`, false);
                    profiler_1.profilerInstance.profileSectionEnd('net-externl', false);
                }
            }
            return result;
        };
        handlers.push(wrappedHandler);
        let expressMethod = {
            GET: 'get',
            POST: 'post',
            PUT: 'put',
            DELETE: 'delete',
            PATCH: 'patch',
        }[method];
        if (!expressMethod) {
            throw new Error(`Fatal: Invalid HTTP method for handler ${method}.`);
        }
        this.externalRoutes.push((app) => {
            app[expressMethod](formattedRoute, handlers);
        });
        if (this.extServer && this.extServer.listening) {
            this._applyExternal();
        }
    }
    _applyExternal() {
        while (this.externalRoutes.length > 0) {
            const routeFn = this.externalRoutes.pop();
            routeFn(this.app);
        }
    }
    setExternalCatchAll(handler) {
        this.externalCatchAll = handler;
    }
    registerExternalGet(route, authHandler, responseHandler) {
        this._registerExternal('GET', route, authHandler, responseHandler);
    }
    registerExternalPost(route, authHandler, responseHandler) {
        this._registerExternal('POST', route, authHandler, responseHandler);
    }
    registerExternalPut(route, authHandler, responseHandler) {
        this._registerExternal('PUT', route, authHandler, responseHandler);
    }
    registerExternalDelete(route, authHandler, responseHandler) {
        this._registerExternal('DELETE', route, authHandler, responseHandler);
    }
    registerExternalPatch(route, authHandler, responseHandler) {
        this._registerExternal('PATCH', route, authHandler, responseHandler);
    }
    registerInternal(route, handler) {
        if (this.internalRoutes[route])
            throw Error('Handler already exists for specified internal route.');
        this.internalRoutes[route] = handler;
    }
    unregisterInternal(route) {
        if (this.internalRoutes[route]) {
            delete this.internalRoutes[route];
        }
    }
}
exports.NetworkClass = NetworkClass;
/** FUNCTIONS */
// export async function init() {
//   mainLogger = logger.getLogger('main')
//   // Make sure we know our IP configuration
//   ipInfo = {
//     externalIp:
//       config.ip.externalIp || (await discoverExternalIp(config.p2p.ipServer)),
//     externalPort: config.ip.externalPort,
//     internalIp: config.ip.internalIp,
//     internalPort: config.ip.internalPort,
//   }
// }
async function init() {
    // Get main logger
    mainLogger = Context_1.logger.getLogger('main');
    // Get default values for IP config
    const defaults = Context_1.defaultConfigs['server']['ip'];
    // Set ipInfo to passed config, automtically if passed 'auto', or to default
    const externalIp = (Context_1.config.ip.externalIp === 'auto' ? await getExternalIp() : Context_1.config.ip.externalIp) || defaults['externalIp'];
    const externalPort = (Context_1.config.ip.externalPort === 'auto' ? await getNextExternalPort(externalIp) : Context_1.config.ip.externalPort) ||
        defaults['externalPort'];
    const internalIp = (Context_1.config.ip.internalIp === 'auto' ? externalIp : Context_1.config.ip.internalIp) || defaults['internalIp'];
    const internalPort = (Context_1.config.ip.internalPort === 'auto' ? await getNextExternalPort(internalIp) : Context_1.config.ip.internalPort) ||
        defaults['internalPort'];
    exports.ipInfo = {
        externalIp,
        externalPort,
        internalIp,
        internalPort,
    };
    if (logger_1.logFlags.info) {
        mainLogger.info('This nodes ipInfo:');
        mainLogger.info(JSON.stringify(exports.ipInfo, null, 2));
    }
}
exports.init = init;
function initNatClient() {
    // Initialize 'nat-api' client if not initialized
    if (!natClient) {
        natClient = new NatAPI();
        natClient['es6'] = {};
        natClient['es6']['externalIp'] = (0, util_1.promisify)(natClient.externalIp.bind(natClient));
        natClient['es6']['map'] = (0, util_1.promisify)(natClient.map.bind(natClient));
        natClient['es6']['destroy'] = (0, util_1.promisify)(natClient.destroy.bind(natClient));
    }
}
async function getExternalIp() {
    initNatClient();
    try {
        const ip = await natClient.es6.externalIp();
        return ip;
    }
    catch (err) {
        mainLogger.warn('Failed to get external IP from gateway:', err.message ? err.message : err);
        try {
            const ip = await discoverExternalIp(Context_1.config.p2p.ipServers);
            return ip;
        }
        catch (err) {
            mainLogger.warn('Failed to get external IP from IP server:', err.message ? err.message : err);
        }
    }
}
async function getNextExternalPort(ip) {
    initNatClient();
    // Get the next available port from the OS and test it
    let [reachable, port] = await wrapTest(new ConnectTest(ip));
    // If port is unreachable attempt to forward it with UPnP, then PMP
    if (reachable === false) {
        const attempts = [{ enablePMP: false }, { enablePMP: true }];
        for (const opts of attempts) {
            if (logger_1.logFlags.info)
                mainLogger.info(`Forwarding ${port} via ${opts.enablePMP ? 'PMP' : 'UPnP'}...`);
            try {
                await natClient.es6.map(Object.assign({ publicPort: port, privatePort: port, protocol: 'TCP' }, opts));
                if (logger_1.logFlags.info)
                    mainLogger.info('  Success!');
                break;
            }
            catch (err) {
                if (logger_1.logFlags.info)
                    mainLogger.info('  Error:', err.message);
            }
        }
    }
    // Test it again
    ;
    [reachable] = await wrapTest(new ConnectTest(ip, port));
    if (reachable) {
        return port;
    }
    else {
        mainLogger.warn('Failed to get next external port');
    }
}
async function wrapTest(test) {
    if (logger_1.logFlags.info)
        mainLogger.info(`Testing ${test.ip}...`);
    test.once('port', (port) => {
        if (logger_1.logFlags.info)
            mainLogger.info(`  Listening on ${port}. Connecting...`);
    });
    let result;
    try {
        const success = await test.start();
        result = [success, test.port];
        if (logger_1.logFlags.info)
            mainLogger.info('  Success!');
    }
    catch (err) {
        if (logger_1.logFlags.info)
            mainLogger.info('  Failed:', err.message ? err.message : err);
        result = [false, test.port];
    }
    return result;
}
class ConnectTest extends events_1.EventEmitter {
    constructor(ip, port) {
        super();
        this.ip = ip;
        this.port = port || -1;
    }
    start() {
        return new Promise((resolve, reject) => {
            // Open a port on 0.0.0.0 (any IP)
            const server = net.createServer(() => { });
            server.unref();
            server.on('error', reject);
            const listenPort = this.port > -1 ? this.port : 0;
            server.listen(listenPort, () => {
                // Get opened port
                const address = server.address();
                this.port = address.port;
                this.emit('port', this.port);
                // Try to connect to given IP at opened port
                const socket = net.createConnection(this.port, this.ip, () => {
                    socket.destroy();
                    server.close(() => resolve(true));
                });
                socket.unref();
                socket.setTimeout(2000);
                socket.on('error', (err) => {
                    socket.destroy();
                    server.close();
                    reject(err);
                });
                socket.on('timeout', () => {
                    socket.destroy();
                    server.close();
                    reject('Connection timed out');
                });
            });
        });
    }
}
/**
 * Gets and offset time from NTP servers.  Returns false if not within
 * config.p2p.syncLimit although this is tuned wrong (too high)
 * and not used
 *
 * @param timeServers
 * @returns
 */
async function checkAndUpdateTimeSyncedOffset(timeServers) {
    // Ignore time check if debug flag is set
    if (Context_1.config.debug.ignoreTimeCheck === true)
        return true;
    // Define sync limit in milliseconds
    const syncLimitMs = Context_1.config.p2p.syncLimit * 1000;
    // Check if local time is within 5 minutes of time servers
    for (const host of timeServers) {
        try {
            const time = await sntp_1.default.time({
                host,
                timeout: 10000,
            });
            //update our offset convert from seconds to MS
            //docs are wrong... time.t seems to be in ms
            ntpOffsetMs = Math.floor(time.t);
            if (Context_1.config.debug.debugNTPBogusDecrements)
                ntpOffsetMs -= 3 * syncLimitMs;
            // Check if the offset is within the acceptable range of positive and negative values
            const isInRange = Math.abs(ntpOffsetMs) <= syncLimitMs;
            lastNTPTimeObj = time;
            //check if ntpOffsetMs is a number
            if (isNaN(ntpOffsetMs)) {
                mainLogger.warn(`NTP Error time.t is NaN ${ntpOffsetMs}`);
                ntpOffsetMs = 0;
            }
            /* prettier-ignore */ console.log(`ntpOffset ${ntpOffsetMs} Offset is in range: ${isInRange} time:${lib_types_1.Utils.safeStringify(time)} host:${host}}`);
            return isInRange;
        }
        catch (e) {
            mainLogger.warn(`Couldn't fetch ntp time from server at ${host}`);
            ntpOffsetMs = 0;
        }
    }
    throw Error('Unable to check local time against time servers.');
}
exports.checkAndUpdateTimeSyncedOffset = checkAndUpdateTimeSyncedOffset;
function shardusGetTime() {
    let time = Date.now();
    if (Context_1.config.p2p.useNTPOffsets === true) {
        time += ntpOffsetMs;
    }
    if (Context_1.config.p2p.useFakeTimeOffsets === true) {
        time += fakeTimeOffsetMs;
    }
    return time;
}
exports.shardusGetTime = shardusGetTime;
function getNetworkTimeOffset() {
    if (Context_1.config.p2p.useNTPOffsets === true) {
        return ntpOffsetMs;
    }
    return ntpOffsetMs;
}
exports.getNetworkTimeOffset = getNetworkTimeOffset;
function calculateFakeTimeOffset(shift, spread) {
    shift = isNaN(shift) ? 0 : shift;
    spread = isNaN(spread) ? 0 : spread;
    const minShift = -5000;
    const maxShift = 5000;
    const minSpread = 0;
    const maxSpread = 5000;
    shift = Math.min(Math.max(shift, minShift), maxShift);
    spread = Math.min(Math.max(spread, minSpread), maxSpread);
    const begin = shift - spread / 2;
    const end = shift + spread / 2;
    fakeTimeOffsetMs = Math.round(begin + (end - begin) * Math.random());
    return fakeTimeOffsetMs;
}
exports.calculateFakeTimeOffset = calculateFakeTimeOffset;
function clearFakeTimeOffset() {
    fakeTimeOffsetMs = 0;
    return fakeTimeOffsetMs;
}
exports.clearFakeTimeOffset = clearFakeTimeOffset;
function getFakeTimeOffset() {
    if (Context_1.config.p2p.useFakeTimeOffsets === true) {
        return fakeTimeOffsetMs;
    }
    return 0;
}
exports.getFakeTimeOffset = getFakeTimeOffset;
function getLastNTPObject() {
    return lastNTPTimeObj;
}
exports.getLastNTPObject = getLastNTPObject;
async function discoverExternalIp(servers) {
    // Figure out if we're behind a NAT
    // Attempt NAT traversal with UPnP
    //
    for (const server of servers) {
        try {
            const { ip } = await httpModule.get(server);
            return ip;
        }
        catch (err) {
            mainLogger.warn(`p2p/Self:discoverExternalIp: Could not discover IP from external IP server ${server}: ` + err.message);
        }
    }
}
function closeServer(server) {
    return new Promise((resolve) => {
        server.close();
        server.unref();
        resolve();
    });
}
//# sourceMappingURL=index.js.map