/**
* @description MeshCentral Intel(R) AMT Local Scanner
* @author Ylian Saint-Hilaire & Joko Sastriawan
* @copyright Intel Corporation 2018
* @license Apache-2.0
* @version v0.0.1
*/

// Construct a Intel AMT Scanner object
module.exports.CreateAmtScanner = function (parent) {
    var obj = {};
    obj.active = false;
    obj.parent = parent;
    obj.net = require('net');
    obj.tls = require('tls');
    obj.dns = require('dns');
    obj.constants = require('constants');
    obj.dgram = require('dgram');
    obj.common = require('./common.js');
    obj.servers = {};
    obj.rserver = {};
    obj.rpacket = null;
    obj.tagToId = {}; // Tag --> { lastpong: time, id: NodeId }
    obj.scanTable = {}; // NodeId --> ScanInfo : { lastping: time, lastpong: time, nodeinfo:{node} }
    obj.scanTableTags = {}; // Tag --> ScanInfo
    obj.pendingSends = []; // We was to stagger the sends using a 10ms timer
    obj.pendingSendTimer = null;
    obj.mainTimer = null;
    obj.nextTag = 0;
    const PeriodicScanTime = 30000; // Interval between scan sweeps
    const PeriodicScanTimeout = 65000; // After this time, timeout the device.

    // Build a RMCP packet with a given tag field
    obj.buildRmcpPing = function (tag) {
        var packet = new Buffer(obj.common.hex2rstr('06000006000011BE80000000'), 'ascii');
        packet[9] = tag;
        return packet;
    }

    // Start scanning for local network Intel AMT computers
    obj.start = function () {
        obj.active = true;
        obj.performScan();
        obj.mainTimer = setInterval(obj.performScan, PeriodicScanTime);
        return obj;
    }

    // Stop scanning for local network Intel AMT computers
    obj.stop = function () {
        obj.active = false;
        for (var i in obj.servers) { obj.servers[i].close(); } // Stop all servers
        obj.servers = {};
        if (obj.mainTimer != null) { clearInterval(obj.mainTimer); obj.mainTimer = null; }
    }

    // Scan for Intel AMT computers using network multicast
    obj.performRangeScan = function (userid, rangestr) {
        if (obj.rpacket == null) { obj.rpacket = obj.buildRmcpPing(0); }
        var range = obj.parseIpv4Range(rangestr);
        //console.log(obj.IPv4NumToStr(range.min), obj.IPv4NumToStr(range.max));
        if (range == null || (range.min > range.max)) return false;
        var rangeinfo = { id: userid, range: rangestr, min: range.min, max: range.max, results: {} };
        obj.rserver[userid] = rangeinfo;
        rangeinfo.server = obj.dgram.createSocket("udp4");
        rangeinfo.server.bind(0);
        rangeinfo.server.on('error', (err) => { console.log(err); });
        rangeinfo.server.on('message', (data, rinfo) => { obj.parseRmcpPacket(data, rinfo, 0, obj.reportMachineState, rangeinfo); });
        rangeinfo.server.on('listening', () => {
            for (var i = rangeinfo.min; i <= rangeinfo.max; i++) { rangeinfo.server.send(obj.rpacket, 623, obj.IPv4NumToStr(i)); }
        });
        rangeinfo.timer = setTimeout(function () {
            obj.parent.DispatchEvent(['*', userid], obj, { action: 'scanamtdevice', range: rangeinfo.range, results: rangeinfo.results, nolog: 1 });
            rangeinfo.server.close();
            delete rangeinfo.server;
            delete rangeinfo;
        }, 3000);
        return true;
    }

    // Parse range, used to parse "ip", "ip/mask" or "ip-ip" notation.
    // Return the start and end value of the scan
    obj.parseIpv4Range = function (range) {
        if (range == undefined || range == null) return null;
        var x = range.split('-');
        if (x.length == 2) { return { min: obj.parseIpv4Addr(x[0]), max: obj.parseIpv4Addr(x[1]) }; }
        x = range.split('/');
        if (x.length == 2) {
            var ip = obj.parseIpv4Addr(x[0]), masknum = parseInt(x[1]), mask = 0;
            if (masknum <= 16 || masknum > 32) return null;
            masknum = 32 - masknum;
            for (var i = 0; i < masknum; i++) { mask = (mask << 1); mask++; }
            return { min: ip & (0xFFFFFFFF - mask), max: (ip & (0xFFFFFFFF - mask)) + mask };
        }
        x = obj.parseIpv4Addr(range);
        if (x == null) return null;
        return { min: x, max: x };
    }

    // Parse IP address. Takes a 
    obj.parseIpv4Addr = function (addr) {
        var x = addr.split('.');
        if (x.length == 4) { return (parseInt(x[0]) << 24) + (parseInt(x[1]) << 16) + (parseInt(x[2]) << 8) + (parseInt(x[3]) << 0); }
        return null;
    }

    // IP address number to string
    obj.IPv4NumToStr = function (num) {
        return ((num >> 24) & 0xFF) + '.' + ((num >> 16) & 0xFF) + '.' + ((num >> 8) & 0xFF) + '.' + (num & 0xFF);
    }

    // Look for all AMT computers that may be locally reachable and poll their presence
    obj.performScan = function () {
        //console.log('performScan');
        if (obj.action == false) { return false; }
        obj.parent.db.getLocalAmtNodes(10, function (err, docs) { // TODO: handler more than 10 computer scan at the same time.
            for (var i in obj.scanTable) { obj.scanTable[i].present = false; }
            if (err == null && docs.length > 0) {
                for (var i in docs) {
                    var doc = docs[i], host = doc.host.toLowerCase();
                    if ((host != '127.0.0.1') && (host != '::1') && (host != 'localhost')) { // Don't scan localhost
                        var scaninfo = obj.scanTable[doc._id];
                        if (scaninfo == undefined) {
                            var tag = obj.nextTag++;
                            obj.scanTableTags[tag] = obj.scanTable[doc._id] = scaninfo = { nodeinfo: doc, present: true, tag: tag, state: 0 };
                            //console.log('Scan ' + host + ', state=' + scaninfo.state + ', delta=' + delta);
                        } else {
                            scaninfo.present = true;
                            var delta = Date.now() - scaninfo.lastpong;
                            //console.log('Rescan ' + host + ', state=' + scaninfo.state + ', delta=' + delta);
                            if ((scaninfo.state == 1) && (delta >= PeriodicScanTimeout)) {
                                // More than 2 minutes without a response, mark the node as unknown state
                                scaninfo.state = 0;
                                obj.parent.ClearConnectivityState(scaninfo.nodeinfo.meshid, scaninfo.nodeinfo._id, 4); // Clear connectivity state
                            } else if ((scaninfo.tcp == null) && ((scaninfo.state == 0) || isNaN(delta) || (delta > PeriodicScanTime))) {
                                // More than 30 seconds without a response, try TCP detection
                                obj.checkTcpPresence(host, (doc.intelamt.tls == 1) ? 16993 : 16992, scaninfo, function (tag, result, version) {
                                    if (result == false) return;
                                    tag.lastpong = Date.now();
                                    if (tag.state == 0) {
                                        tag.state = 1;
                                        obj.parent.SetConnectivityState(tag.nodeinfo.meshid, tag.nodeinfo._id, tag.lastpong, 4, 7); // Report power state as "present" (7).
                                        if (version != null) { obj.changeAmtState(tag.nodeinfo._id, version, 2, tag.nodeinfo.intelamt.tls); }
                                    } 
                                });
                            }
                        }
                        // Start scanning this node
                        scaninfo.lastping = Date.now();
                        obj.checkAmtPresence(doc.host, scaninfo.tag);
                    }
               }
            }
            for (var i in obj.scanTable) {
                if (obj.scanTable[i].present == false) {
                    // Stop scanning this node
                    delete obj.scanTableTags[obj.scanTable[i].tag];
                    delete obj.scanTable[i];
                }
            }
        });
        return true;
    }

    // Check the presense of a specific Intel AMT computer using RMCP
    obj.checkAmtPresence = function (host, tag) {
        var serverid = Math.floor(tag / 255);
        var servertag = (tag % 255);
        var packet = obj.buildRmcpPing(servertag);
        var server = obj.servers[serverid];
        if (server == undefined) {
            // Start new server
            server = obj.dgram.createSocket('udp4');
            server.on('error', (err) => { });
            server.on('message', (data, rinfo) => { obj.parseRmcpPacket(data, rinfo, serverid, obj.changeConnectState, null); });
            server.on('listening', () => {
                obj.pendingSends.push([ server, packet, host ]);
                if (obj.pendingSendTimer == null) { obj.pendingSendTimer = setInterval(obj.sendPendingPacket, 10); }
            });
            server.bind(0);
            obj.servers[serverid] = server;
        } else {
            // Use existing server
            obj.pendingSends.push([ server, packet, host ]);
            if (obj.pendingSendTimer == null) { obj.pendingSendTimer = setInterval(obj.sendPendingPacket, 10); }
        }
    }

    // Send a pending RMCP packet
    obj.sendPendingPacket = function() {
        try {
            var p = obj.pendingSends.shift();
            if (p != undefined) {
                p[0].send(p[1], 623, p[2]);
                p[0].send(p[1], 623, p[2]);
            } else {
                clearInterval(obj.pendingSendTimer);
                obj.pendingSendTimer = null;
            }
        } catch (e) { }
    }

    // Parse RMCP packet
    obj.parseRmcpPacket = function (data, rinfo, serverid, func, user) {
        if (data == null || data.length < 20) return;
        if (((data[12] == 0) || (data[13] != 0) || (data[14] != 1) || (data[15] != 0x57)) && (data[21] & 32)) {
            var servertag = data[9];
            var tag = (serverid * 255) + servertag;
            var minorVersion = data[18] & 0x0F;
            var majorVersion = (data[18] >> 4) & 0x0F;
            var provisioningState = data[19] & 0x03; // Pre = 0, In = 1, Post = 2
            
            var openPort = (data[16] * 256) + data[17];
            var dualPorts = ((data[19] & 0x04) != 0) ? true : false;
            var openPorts = [openPort];
            if (dualPorts == true) { openPorts = [16992, 16993]; }
            if (provisioningState <= 2) { func(tag, minorVersion, majorVersion, provisioningState, openPort, dualPorts, rinfo, user); }
        }
    }

    // Use the RMCP packet to change the computer state
    obj.changeConnectState = function (tag, minorVersion, majorVersion, provisioningState, openPort, dualPorts, rinfo, user) {
        //var provisioningStates = { 0: 'Pre', 1: 'in', 2: 'Post' };
        //var provisioningStateStr = provisioningStates[provisioningState];
        //console.log('Intel AMT ' + majorVersion + '.' + minorVersion + ', ' + provisioningStateStr + '-Provisioning at ' + rinfo.address + ', Open Ports: [' + openPort + '], tag: ' + tag);
        var scaninfo = obj.scanTableTags[tag];
        if (scaninfo != undefined) {
            scaninfo.lastpong = Date.now();
            if (scaninfo.state == 0) {
                scaninfo.state = 1;
                scaninfo.nodeinfo.intelamt.tls = (((openPort == 16993) || (dualPorts == true)) ? 1 : 0);
                scaninfo.nodeinfo.intelamt.ver = majorVersion + '.' + minorVersion;
                scaninfo.nodeinfo.intelamt.state = provisioningState;
                obj.parent.SetConnectivityState(scaninfo.nodeinfo.meshid, scaninfo.nodeinfo._id, scaninfo.lastpong, 4, 7); // Report power state as "present" (7).
                obj.changeAmtState(scaninfo.nodeinfo._id, scaninfo.nodeinfo.intelamt.ver, provisioningState, scaninfo.nodeinfo.intelamt.tls);
            }
        }
    }

	// Use the RMCP packet to change the computer state
	obj.reportMachineState = function (tag, minorVersion, majorVersion, provisioningState, openPort, dualPorts, rinfo, user) {
	    //var provisioningStates = { 0: 'Pre', 1: 'in', 2: 'Post' };
	    //var provisioningStateStr = provisioningStates[provisioningState];
	    //console.log(rinfo.address + ': Intel AMT ' + majorVersion + '.' + minorVersion + ', ' + provisioningStateStr + '-Provisioning, Open Ports: [' + openPorts.join(', ') + ']');
        obj.dns.reverse(rinfo.address, function (err, hostname) {
            if ((err != undefined) && (hostname != undefined)) {
	            user.results[rinfo.address] = { ver: majorVersion + '.' + minorVersion, tls: (((openPort == 16993) || (dualPorts == true)) ? 1 : 0), state: provisioningState, hostname: hostname[0] };
	        } else {
	            user.results[rinfo.address] = { ver: majorVersion + '.' + minorVersion, tls: (((openPort == 16993) || (dualPorts == true)) ? 1 : 0), state: provisioningState, hostname: rinfo.address };
	        }
	    });
	}

    // Change Intel AMT information in the database and event the changes
    obj.changeAmtState = function (nodeid, version, provisioningState, tls) {
        //console.log('changeAmtState', nodeid, version, provisioningState, tls);
        obj.parent.db.Get(nodeid, function (err, nodes) {
            if (nodes.length != 1) return;
            var node = nodes[0];

            // Get the mesh for this device
            obj.parent.db.Get(node.meshid, function (err, meshes) {
                if (meshes.length != 1) return;
                var mesh = meshes[0];

                // Ready the node change event
                var changes = [], event = { etype: 'node', action: 'changenode', nodeid: node._id };
                event.msg = +": ";

                // Make the change & save
                var change = false;
                if (node.intelamt == undefined) { node.intelamt = {}; }
                if (node.intelamt.tls != tls) { node.intelamt.tls = tls; change = true; changes.push(tls==1?'TLS':'NoTLS'); }
                if (obj.compareAmtVersionStr(node.intelamt.ver, version)) { node.intelamt.ver = version; change = true; changes.push('AMT Version ' + version); }
                if (node.intelamt.state != provisioningState) { node.intelamt.state = provisioningState; change = true; changes.push('AMT State'); }
                if (change == true) {
                    // Make the change in the database
                    obj.parent.db.Set(node);

                    // Event the node change
                    event.msg = 'Intel&reg; AMT changed device ' + node.name + ' from mesh ' + mesh.name + ': ' + changes.join(', ');
                    var node2 = obj.parent.common.Clone(node);
                    if (node2.intelamt && node2.intelamt.pass) delete node2.intelamt.pass; // Remove the Intel AMT password before eventing this.
                    event.node = node2;
                    obj.parent.DispatchEvent(['*', node.meshid], obj, event);
                }
            });
        });
    }

    // Return true if we should change the Intel AMT version number
    obj.compareAmtVersionStr = function (oldVer, newVer) {
        if (oldVer == newVer) return false; // Versions are same already, don't update.
        if (newVer == undefined || newVer == null) return false; // New version is bad, don't update it.
        if (oldVer == undefined || oldVer == null) return true; // Old version is no good anyway, update it.
        var oldVerArr = oldVer.split('.');
        var newVerArr = newVer.split('.');
        if ((oldVerArr.length < 2) || (newVerArr.length < 2)) return false;
        if ((oldVerArr[0] != newVerArr[0]) || (oldVerArr[1] != newVerArr[1])) return true;
        if (newVerArr.length > oldVerArr.length) return true;
        if ((newVerArr.length == 3) && (oldVerArr.length == 3) && (oldVerArr[2] != newVerArr[2])) return true;
        return false;
    }

    // Check that we can connect TCP to a given port
    obj.checkTcpPresence = function (host, port, scaninfo, func) {
        //console.log('checkTcpPresence(' + host + ':' + port + ')');
        try {
            var client;
            if (port == 16992) {
                // Connect using TCP
                client = new obj.net.Socket();
                client.connect(port, host, function () { this.write('GET / HTTP/1.1\r\nhost: ' + host + '\r\n\r\n'); });
            } else {
                // Connect using TLS, we will switch from default TLS to TLS1-only and back if we get a connection error to support older Intel AMT.
                if (scaninfo.tlsoption == null) { scaninfo.tlsoption = 0; }
                client = obj.tls.connect(port, host, scaninfo.tlsoption == 1 ? { secureProtocol: 'TLSv1_method', rejectUnauthorized: false, ciphers: 'RSA+AES:!aNULL:!MD5:!DSS', secureOptions: obj.constants.SSL_OP_NO_SSLv2 | obj.constants.SSL_OP_NO_SSLv3 | obj.constants.SSL_OP_NO_COMPRESSION | obj.constants.SSL_OP_CIPHER_SERVER_PREFERENCE } : { rejectUnauthorized: false, ciphers: 'RSA+AES:!aNULL:!MD5:!DSS', secureOptions: obj.constants.SSL_OP_NO_SSLv2 | obj.constants.SSL_OP_NO_SSLv3 | obj.constants.SSL_OP_NO_COMPRESSION | obj.constants.SSL_OP_CIPHER_SERVER_PREFERENCE }, function () { this.write('GET / HTTP/1.1\r\nhost: ' + host + '\r\n\r\n'); });
            }
            client.scaninfo = scaninfo;
            client.func = func;
            client.port = port;
            client.setTimeout(10000);
            client.on('data', function (data) { var version = obj.getIntelAmtVersionFromHeaders(data.toString()); if (this.scaninfo.tcp != null) { delete this.scaninfo.tcp; try { this.destroy(); } catch (ex) { } this.func(this.scaninfo, version != null, version); } });
            client.on('error', function () { if (this.scaninfo.tlsoption == 0) { this.scaninfo.tlsoption = 1; } else if (this.scaninfo.tlsoption == 1) { this.scaninfo.tlsoption = 0; } if (this.scaninfo.tcp != null) { delete this.scaninfo.tcp; try { this.destroy(); } catch (ex) { } this.func(this.scaninfo, false); } });
            client.on('timeout', function () { if (this.scaninfo.tcp != null) { delete this.scaninfo.tcp; try { this.destroy(); } catch (ex) { } this.func(this.scaninfo, false); } });
            client.on('close', function () { if (this.scaninfo.tcp != null) { delete this.scaninfo.tcp; try { this.destroy(); } catch (ex) { } this.func(this.scaninfo, false); } });
            client.on('end', function () { if (this.scaninfo.tcp != null) { delete this.scaninfo.tcp; try { this.destroy(); } catch (ex) { } this.func(this.scaninfo, false); } });
            scaninfo.tcp = client;
        } catch (ex) { console.log(ex); }
    }

    // Return the Intel AMT version from the HTTP headers. Return null if nothing is found.
    obj.getIntelAmtVersionFromHeaders = function (headers) {
        if (headers == null || headers.length == 0) return null;
        var lines = headers.split('\r\n');
        for (var i in lines) {
            // Look for the Intel AMT version
            if (lines[i].substring(0, 46) == 'Server: Intel(R) Active Management Technology ') {
                // We need to check that the Intel AMT version is correct, in the "a.b.c" format
                var ver = lines[i].substring(46), splitver = ver.split('.');
                if ((splitver.length == 3 || splitver.length == 4) && ('' + parseInt(splitver[0]) === splitver[0]) && ('' + parseInt(splitver[1]) === splitver[1]) && ('' + parseInt(splitver[2]) === splitver[2])) { return (splitver[0] + '.' + splitver[1] + '.' + splitver[2]); }
            }
        }
        return null;
    }

    //console.log(obj.getIntelAmtVersionFromHeaders("HTTP/1.1 303 See Other\r\nLocation: /logon.htm\r\nContent-Length: 0\r\nServer: Intel(R) Active Management Technology 7.1.91\r\n\r\n"));

    return obj;
}