/**
* @description APF/CIRA Client for duktape
* @author Joko Sastriawan
* @copyright Intel Corporation 2019
* @license Apache-2.0
* @version v0.0.1
*/

function CreateAPFClient(parent, args) {    
    var obj = {};
    obj.parent = parent;
    obj.args = args;
    obj.http = require('http');
    //obj.common = require('common');    
    obj.net = require('net');    
    obj.forwardClient = null;
    obj.downlinks = {};
    obj.pfwd_idx = 0;
    // keep alive timer
    obj.timer = null;
    
    // some function copied from common.js
    function ReadInt(v, p) { 
        return (v.charCodeAt(p) * 0x1000000) + (v.charCodeAt(p + 1) << 16) + (v.charCodeAt(p + 2) << 8) + v.charCodeAt(p + 3); 
    }; // We use "*0x1000000" instead of "<<24" because the shift converts the number to signed int32.
    
    function IntToStr(v) { 
        return String.fromCharCode((v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF); 
    };

    function hex2rstr(d) {
        var r = '', m = ('' + d).match(/../g), t;
        while (t = m.shift()) { r += String.fromCharCode('0x' + t); }
        return r;
    };

    // Convert decimal to hex
    function char2hex(i) { return (i + 0x100).toString(16).substr(-2).toUpperCase(); };

    // Convert a raw string to a hex string
    function rstr2hex(input) {
        var r = '', i;
        for (i = 0; i < input.length; i++) { r += char2hex(input.charCodeAt(i)); }
        return r;
    };

    function d2h(d) {
        return (d / 256 + 1 / 512).toString(16).substring(2, 4);
    }

    function buf2hex(input) {
        var r = '', i;        
        for (i = 0; i < input.length; i++) { r += d2h(input[i]); }
        return r;
    };

    
    function Debug(str) {
        if (obj.parent.debug) {
            console.log(str);
        }
    }
    // CIRA state
    var CIRASTATE = {
        INITIAL: 0,
        PROTOCOL_VERSION_SENT: 1,
        AUTH_SERVICE_REQUEST_SENT: 2,
        AUTH_REQUEST_SENT: 3,
        PFWD_SERVICE_REQUEST_SENT: 4,
        GLOBAL_REQUEST_SENT: 5,
        FAILED: -1
    }
    obj.cirastate = CIRASTATE.INITIAL;

    // REDIR state
    var REDIR_TYPE = {
        REDIR_UNKNOWN: 0,
        REDIR_SOL: 1,
        REDIR_KVM: 2,
        REDIR_IDER: 3
    }

    // redirection start command
    obj.RedirectStartSol = String.fromCharCode(0x10, 0x00, 0x00, 0x00, 0x53, 0x4F, 0x4C, 0x20);
    obj.RedirectStartKvm = String.fromCharCode(0x10, 0x01, 0x00, 0x00, 0x4b, 0x56, 0x4d, 0x52);
    obj.RedirectStartIder = String.fromCharCode(0x10, 0x00, 0x00, 0x00, 0x49, 0x44, 0x45, 0x52);


    // AMT forwarded port list for non-TLS mode
    var pfwd_ports = [16992, 623, 16994, 5900];
    // protocol definitions
    var APFProtocol = {
        UNKNOWN: 0,
        DISCONNECT: 1,
        SERVICE_REQUEST: 5,
        SERVICE_ACCEPT: 6,
        USERAUTH_REQUEST: 50,
        USERAUTH_FAILURE: 51,
        USERAUTH_SUCCESS: 52,
        GLOBAL_REQUEST: 80,
        REQUEST_SUCCESS: 81,
        REQUEST_FAILURE: 82,
        CHANNEL_OPEN: 90,
        CHANNEL_OPEN_CONFIRMATION: 91,
        CHANNEL_OPEN_FAILURE: 92,
        CHANNEL_WINDOW_ADJUST: 93,
        CHANNEL_DATA: 94,
        CHANNEL_CLOSE: 97,
        PROTOCOLVERSION: 192,
        KEEPALIVE_REQUEST: 208,
        KEEPALIVE_REPLY: 209,
        KEEPALIVE_OPTIONS_REQUEST: 210,
        KEEPALIVE_OPTIONS_REPLY: 211
    }

    var APFDisconnectCode = {
        HOST_NOT_ALLOWED_TO_CONNECT: 1,
        PROTOCOL_ERROR: 2,
        KEY_EXCHANGE_FAILED: 3,
        RESERVED: 4,
        MAC_ERROR: 5,
        COMPRESSION_ERROR: 6,
        SERVICE_NOT_AVAILABLE: 7,
        PROTOCOL_VERSION_NOT_SUPPORTED: 8,
        HOST_KEY_NOT_VERIFIABLE: 9,
        CONNECTION_LOST: 10,
        BY_APPLICATION: 11,
        TOO_MANY_CONNECTIONS: 12,
        AUTH_CANCELLED_BY_USER: 13,
        NO_MORE_AUTH_METHODS_AVAILABLE: 14,
        INVALID_CREDENTIALS: 15,
        CONNECTION_TIMED_OUT: 16,
        BY_POLICY: 17,
        TEMPORARILY_UNAVAILABLE: 18
    }

    var APFChannelOpenFailCodes = {
        ADMINISTRATIVELY_PROHIBITED: 1,
        CONNECT_FAILED: 2,
        UNKNOWN_CHANNEL_TYPE: 3,
        RESOURCE_SHORTAGE: 4,
    }

    var APFChannelOpenFailureReasonCode = {
        AdministrativelyProhibited: 1,
        ConnectFailed: 2,
        UnknownChannelType: 3,
        ResourceShortage: 4,
    }

    obj.onSecureConnect = function onSecureConnect(resp, ws, head) {
        Debug("APF Secure WebSocket connected.");
        //console.log(JSON.stringify(resp));                
        obj.forwardClient.tag = { accumulator: [] };
        obj.forwardClient.ws = ws;                
        obj.forwardClient.ws.on('end', function () {
            Debug("APF: Connection is closing.");
            if (obj.timer != null) {
                clearInterval(obj.timer);
                obj.timer = null;
            }
        });

        obj.forwardClient.ws.on('data', function (data) {
            obj.forwardClient.tag.accumulator += hex2rstr(buf2hex(data));                        
            try {
                var len = 0;
                do {
                    len = ProcessData(obj.forwardClient);
                    if (len > 0) {                        
                        obj.forwardClient.tag.accumulator = obj.forwardClient.tag.accumulator.slice(len);
                    }
                    if (obj.cirastate == CIRASTATE.FAILED) {
                        Debug("APF: in a failed state, destroying socket.")
                        obj.forwardClient.ws.end();
                    }
                } while (len > 0);
            } catch (e) {
                Debug(e);
            }
        });        
        
        obj.forwardClient.ws.on('error', function (e) {
            Debug("APF: Connection error, ending connecting.");
            if (obj.timer != null) {
                clearInterval(obj.timer);
                obj.timer = null;
            }
        });        

        obj.state = CIRASTATE.INITIAL;
        SendProtocolVersion(obj.forwardClient.ws, obj.args.clientuuid);
        SendServiceRequest(obj.forwardClient.ws, 'auth@amt.intel.com');
    }

    function guidToStr(g) { return g.substring(6, 8) + g.substring(4, 6) + g.substring(2, 4) + g.substring(0, 2) + "-" + g.substring(10, 12) + g.substring(8, 10) + "-" + g.substring(14, 16) + g.substring(12, 14) + "-" + g.substring(16, 20) + "-" + g.substring(20); }
    function strToGuid(s) {
        s = s.replace(/-/g, '');
        var ret = s.substring(6, 8) + s.substring(4, 6) + s.substring(2, 4) + s.substring(0, 2);
        ret += s.substring(10, 12) + s.substring(8, 10) + s.substring(14, 16) + s.substring(12, 14) + s.substring(16, 20) + s.substring(20);
        return ret;
    }

    function binzerostring(len) {
        var res='';
        for (var l=0; l< len ; l++) {
            res+=String.fromCharCode(0 & 0xFF);
        }
        return res;
    }


    
    function SendProtocolVersion(socket, uuid) {        
        var buuid = strToGuid(uuid);
        var data = String.fromCharCode(APFProtocol.PROTOCOLVERSION) + '' + IntToStr(1) + IntToStr(0) + IntToStr(0) + hex2rstr(buuid) + binzerostring(64);
        socket.write(data);        
        Debug("APF: Send protocol version 1 0 " + uuid);        
        obj.cirastate = CIRASTATE.PROTOCOL_VERSION_SENT;
    }

    function SendServiceRequest(socket, service) {
        var data = String.fromCharCode(APFProtocol.SERVICE_REQUEST) + IntToStr(service.length) + service;
        socket.write(data);
        Debug("APF: Send service request " + service);
        if (service == 'auth@amt.intel.com') {
            obj.cirastate = CIRASTATE.AUTH_SERVICE_REQUEST_SENT;
        } else if (service == 'pfwd@amt.intel.com') {
            obj.cirastate = CIRASTATE.PFWD_SERVICE_REQUEST_SENT;
        }
    }

    function SendUserAuthRequest(socket, user, pass) {
        var service = "pfwd@amt.intel.com";
        var data = String.fromCharCode(APFProtocol.USERAUTH_REQUEST) + IntToStr(user.length) + user + IntToStr(service.length) + service;
        //password auth
        data += IntToStr(8) + 'password';
        data += binzerostring(1) + IntToStr(pass.length) + pass;
        socket.write(data);
        Debug("APF: Send username password authentication to MPS");
        obj.cirastate = CIRASTATE.AUTH_REQUEST_SENT;
    }

    function SendGlobalRequestPfwd(socket, amthostname, amtport) {
        var tcpipfwd = 'tcpip-forward';
        var data = String.fromCharCode(APFProtocol.GLOBAL_REQUEST) + IntToStr(tcpipfwd.length) + tcpipfwd + binzerostring(1, 1);
        data += IntToStr(amthostname.length) + amthostname + IntToStr(amtport);
        socket.write(data);
        Debug("APF: Send tcpip-forward " + amthostname + ":" + amtport);
        obj.cirastate = CIRASTATE.GLOBAL_REQUEST_SENT;
    }

    function SendKeepAliveRequest(socket) {
        var data = String.fromCharCode(APFProtocol.KEEPALIVE_REQUEST) + IntToStr(255);
        socket.write(data);
        Debug("APF: Send keepalive request");
    }

    function SendKeepAliveReply(socket, cookie) {
        var data = String.fromCharCode(APFProtocol.KEEPALIVE_REPLY) + IntToStr(cookie);
        socket.write(data);
        Debug("APF: Send keepalive reply");
    }

    function ProcessData(socket) {
        var cmd = socket.tag.accumulator.charCodeAt(0);
        var len = socket.tag.accumulator.length;
        var data = socket.tag.accumulator;
        if (len == 0) { return 0; }
        // respond to MPS according to obj.cirastate
        switch (cmd) {
            case APFProtocol.SERVICE_ACCEPT: {
                var slen = ReadInt(data, 1);
                var service = data.substring(5, 6 + slen);
                Debug("APF: Service request to " + service + " accepted.");
                if (service == 'auth@amt.intel.com') {
                    if (obj.cirastate >= CIRASTATE.AUTH_SERVICE_REQUEST_SENT) {
                        SendUserAuthRequest(socket.ws, obj.args.mpsuser, obj.args.mpspass);
                    }
                } else if (service == 'pfwd@amt.intel.com') {
                    if (obj.cirastate >= CIRASTATE.PFWD_SERVICE_REQUEST_SENT) {
                        SendGlobalRequestPfwd(socket.ws, obj.args.clientname, pfwd_ports[obj.pfwd_idx++]);
                    }
                }
                return 5 + slen;
            }
            case APFProtocol.REQUEST_SUCCESS: {
                if (len >= 5) {
                    var port = ReadInt(data, 1);
                    Debug("APF: Request to port forward " + port + " successful.");
                    // iterate to pending port forward request
                    if (obj.pfwd_idx < pfwd_ports.length) {
                        SendGlobalRequestPfwd(socket.ws, obj.args.clientname, pfwd_ports[obj.pfwd_idx++]);
                    } else {
                        // no more port forward, now setup timer to send keep alive
                        Debug("APF: Start keep alive for every " + obj.args.mpskeepalive + " ms.");
                        obj.timer = setInterval(function () {
                            SendKeepAliveRequest(obj.forwardClient.ws);
                        }, obj.args.mpskeepalive);//
                    }
                    return 5;
                }
                Debug("APF: Request successful.");
                return 1;
            }
            case APFProtocol.USERAUTH_SUCCESS: {
                Debug("APF: User Authentication successful");
                // Send Pfwd service request
                SendServiceRequest(socket.ws, 'pfwd@amt.intel.com');
                return 1;
            }
            case APFProtocol.USERAUTH_FAILURE: {
                Debug("APF: User Authentication failed");
                obj.cirastate = CIRASTATE.FAILED;
                return 14;
            }
            case APFProtocol.KEEPALIVE_REQUEST: {
                Debug("APF: Keep Alive Request with cookie: " + ReadInt(data, 1));
                SendKeepAliveReply(socket.ws, ReadInt(data, 1));
                return 5;
            }
            case APFProtocol.KEEPALIVE_REPLY: {
                Debug("APF: Keep Alive Reply with cookie: " + ReadInt(data, 1));
                return 5;
            }
            // Channel management
            case APFProtocol.CHANNEL_OPEN: {
                //parse CHANNEL OPEN request
                var p_res = parseChannelOpen(data);
                Debug("APF: CHANNEL_OPEN request: " + JSON.stringify(p_res));
                // Check if target port is in pfwd_ports
                if (pfwd_ports.indexOf(p_res.target_port) >= 0) {
                    // connect socket to that port
                    obj.downlinks[p_res.sender_chan] = obj.net.createConnection({ host: obj.args.clientaddress, port: p_res.target_port }, function () {
                        //obj.downlinks[p_res.sender_chan].setEncoding('binary');//assume everything is binary, not interpreting
                        SendChannelOpenConfirm(socket.ws, p_res);
                    });

                    obj.downlinks[p_res.sender_chan].on('data', function (ddata) {
                        //Relay data to fordwardclient
                        SendChannelData(socket.ws, p_res.sender_chan, ddata.length, ddata);
                    });

                    obj.downlinks[p_res.sender_chan].on('error', function (e) {
                        Debug("Downlink connection error: " + e);
                    });

                    obj.downlinks[p_res.sender_chan].on('end', function () {
                        if (obj.downlinks[p_res.sender_chan]) {
                            try {
                                Debug("Socket ends.");
                                SendChannelClose(socket.ws, p_res.sender_chan);
                                // add some delay before removing... otherwise race condition
                                setTimeout(function () { delete obj.downlinks[p_res.sender_chan];},100);
                            } catch (e) {
                                Debug("Downlink connection exception: " + e);
                            }
                        }
                    });
                } else {
                    SendChannelOpenFailure(socket.ws, p_res);
                }
                return p_res.len;
            }
            case APFProtocol.CHANNEL_OPEN_CONFIRMATION: {
                Debug("APF: CHANNEL_OPEN_CONFIRMATION");
                return 17;
            }
            case APFProtocol.CHANNEL_CLOSE: {
                var rcpt_chan = ReadInt(data, 1);
                Debug("APF: CHANNEL_CLOSE: " + rcpt_chan);
                SendChannelClose(socket.ws, rcpt_chan);
                try {
                    obj.downlinks[rcpt_chan].end();
                    delete obj.downlinks[rcpt_chan];
                } catch (e) { }
                return 5;
            }
            case APFProtocol.CHANNEL_DATA: {
                Debug("APF: CHANNEL_DATA: " + JSON.stringify(rstr2hex(data)));
                var rcpt_chan = ReadInt(data, 1);
                var chan_data_len = ReadInt(data, 5);
                var chan_data = data.substring(9, 9 + chan_data_len);
                if (obj.downlinks[rcpt_chan]) {
                    try {
                        obj.downlinks[rcpt_chan].write(chan_data, 'binary', function () {
                            Debug("Write completed.");
                            SendChannelWindowAdjust(socket.ws, rcpt_chan, chan_data_len);//I have full window capacity now
                        });
                    } catch (e) {
                        Debug("Cannot forward data to downlink socket.");
                    }
                }
                return 9 + chan_data_len;
            }
            case APFProtocol.CHANNEL_WINDOW_ADJUST: {
                Debug("APF: CHANNEL_WINDOW_ADJUST ");
                return 9;
            }
            default: {
                Debug("CMD: " + cmd + " is not implemented.");
                obj.cirastate = CIRASTATE.FAILED;
                return 0;
            }
        }
    }

    function parseChannelOpen(data) {
        var result = {
            len: 0, //to be filled later
            cmd: APFProtocol.CHANNEL_OPEN,
            chan_type: "", //to be filled later
            sender_chan: 0, //to be filled later
            window_size: 0, //to be filled later
            target_address: "", //to be filled later
            target_port: 0, //to be filled later
            origin_address: "", //to be filled later
            origin_port: 0, //to be filled later
        };
        var chan_type_slen = ReadInt(data, 1);
        result.chan_type = data.substring(5, 5 + chan_type_slen);
        result.sender_chan = ReadInt(data, 5 + chan_type_slen);
        result.window_size = ReadInt(data, 9 + chan_type_slen);
        var c_len = ReadInt(data, 17 + chan_type_slen);
        result.target_address = data.substring(21 + chan_type_slen, 21 + chan_type_slen + c_len);
        result.target_port = ReadInt(data, 21 + chan_type_slen + c_len);
        var o_len = ReadInt(data, 25 + chan_type_slen + c_len);
        result.origin_address = data.substring(29 + chan_type_slen + c_len, 29 + chan_type_slen + c_len + o_len);
        result.origin_port = ReadInt(data, 29 + chan_type_slen + c_len + o_len);
        result.len = 33 + chan_type_slen + c_len + o_len;
        return result;
    }
    function SendChannelOpenFailure(socket, chan_data) {
        var data = String.fromCharCode(APFProtocol.CHANNEL_OPEN_FAILURE) + IntToStr(chan_data.sender_chan)
            + IntToStr(2) + IntToStr(0) + IntToStr(0);
        socket.write(data);
        Debug("APF: Send ChannelOpenFailure");
    }
    function SendChannelOpenConfirm(socket, chan_data) {
        var data = String.fromCharCode(APFProtocol.CHANNEL_OPEN_CONFIRMATION) + IntToStr(chan_data.sender_chan)
            + IntToStr(chan_data.sender_chan) + IntToStr(chan_data.window_size) + IntToStr(0xFFFFFFFF);
        socket.write(data);
        Debug("APF: Send ChannelOpenConfirmation");
    }

    function SendChannelWindowAdjust(socket, chan, size) {
        var data = String.fromCharCode(APFProtocol.CHANNEL_WINDOW_ADJUST) + IntToStr(chan) + IntToStr(size);
        socket.write(data);
        Debug("APF: Send ChannelWindowAdjust: " + rstr2hex(data));
    }

    function SendChannelData(socket, chan, len, data) {
        var buf = String.fromCharCode(APFProtocol.CHANNEL_DATA) + IntToStr(chan) + IntToStr(len) + data;
        socket.write(buf);
        Debug("APF: Send ChannelData: " + rstr2hex(buf));
    }

    function SendChannelClose(socket, chan) {
        var buf = String.fromCharCode(APFProtocol.CHANNEL_CLOSE) + IntToStr(chan);
        socket.write(buf);
        Debug("APF: Send ChannelClose ");
    }

    obj.connect = function () {
        if (obj.forwardClient != null) {
            try {
                obj.forwardClient.ws.end();
            } catch (e) {
                Debug(e);
            }
            //obj.forwardClient = null;
        }
        obj.cirastate = CIRASTATE.INITIAL;
        obj.pfwd_idx = 0;
        
        //obj.forwardClient = new obj.ws(obj.args.mpsurl, obj.tlsoptions);
        //obj.forwardClient.on("open", obj.onSecureConnect);


        var wsoptions = obj.http.parseUri(obj.args.mpsurl);        
        wsoptions.rejectUnauthorized = 0;
        obj.forwardClient = obj.http.request(wsoptions);        
        obj.forwardClient.upgrade = obj.onSecureConnect;        
        obj.forwardClient.end(); // end request, trigger completion of HTTP request
    }

    obj.disconnect = function () {
        try {
            obj.forwardClient.ws.end();
        } catch (e) {
            Debug(e);
        }
    }

    return obj;
}

module.exports = CreateAPFClient;