From a78d3ca9b6e9f00d9510065c7d05d0a02cd1acb3 Mon Sep 17 00:00:00 2001 From: jsastriawan Date: Mon, 16 Sep 2019 07:58:30 -0700 Subject: [PATCH] Add apf client for duktape and minor debug removal. --- agents/modules_meshcore/apfclient.js | 496 +++++++++++++++++++++++++++ apfserver.js | 21 +- 2 files changed, 508 insertions(+), 9 deletions(-) create mode 100644 agents/modules_meshcore/apfclient.js diff --git a/agents/modules_meshcore/apfclient.js b/agents/modules_meshcore/apfclient.js new file mode 100644 index 00000000..3f26c01a --- /dev/null +++ b/agents/modules_meshcore/apfclient.js @@ -0,0 +1,496 @@ +/** +* @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 { + SendChannelClose(socket.ws, p_res.sender_chan); + delete obj.downlinks[p_res.sender_chan]; + } 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(Buffer.from(buf, 'binary')); + Debug("APF: Send ChannelData: " + rstr2hex(buf)); + } + + function SendChannelClose(socket, chan) { + var buf = String.fromCharCode(APFProtocol.CHANNEL_CLOSE) + IntToStr(chan); + socket.write(Buffer.from(buf, 'binary')); + Debug("APF: Send ChannelClose: " + rstr2hex(buf)); + } + + 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; \ No newline at end of file diff --git a/apfserver.js b/apfserver.js index 344a2392..72ea9041 100644 --- a/apfserver.js +++ b/apfserver.js @@ -132,17 +132,20 @@ module.exports.CreateApfServer = function (parent, db, args) { }; } - obj.onConnection = function(socket) { - console.log("Here"); + obj.onConnection = function(socket) { connectionCount++; // treat APS over WS like tlsoffload APF socket.tag = { first: true, clientCert: null, accumulator: "", activetunnels: 0, boundPorts: [], socket: socket, host: null, nextchannelid: 4, channels: {}, nextsourceport: 0 }; parent.debug('apf', "New APF connection"); + parent.debug('apf',"WS Extensions:"+socket.extensions); + parent.debug('apf',"WS Binary type:"+socket.binaryType); + + socket._socket.on('data', function(chunk) { console.log(chunk.toString('hex'))}); // Setup the APF keep alive timer // Websocket does not have timout // socket.setTimeout(MAX_IDLE); - //socket.on("timeout", () => { ciraTimeoutCount++; parent.debug('mps', "APF timeout, disconnecting."); try { socket.terminate(); } catch (e) { } }); + //socket.on("timeout", () => { ciraTimeoutCount++; parent.debug('apf', "APF timeout, disconnecting."); try { socket.terminate(); } catch (e) { } }); //use on message instead because of websocket socket.on("message", function (data) { // use the same debug flag like APF @@ -206,13 +209,13 @@ module.exports.CreateApfServer = function (parent, db, args) { parent.debug('apfcmd', 'USERAUTH_REQUEST user=' + username + ', service=' + serviceName + ', method=' + methodName + ', password=' + password); // Check the APF password - if ((args.mpspass != null) && (password != args.mpspass)) { incorrectPasswordCount++; parent.debug('mps', 'Incorrect password', username, password); SendUserAuthFail(socket); return -1; } + if ((args.mpspass != null) && (password != args.mpspass)) { incorrectPasswordCount++; parent.debug('apf', 'Incorrect password', username, password); SendUserAuthFail(socket); return -1; } // Check the APF username, which should be the start of the MeshID. - if (usernameLen != 16) { badUserNameLengthCount++; parent.debug('mps', 'Username length not 16', username, password); SendUserAuthFail(socket); return -1; } + if (usernameLen != 16) { badUserNameLengthCount++; parent.debug('apf', 'Username length not 16', username, password); SendUserAuthFail(socket); return -1; } var meshIdStart = '/' + username, mesh = null; if (obj.parent.webserver.meshes) { for (var i in obj.parent.webserver.meshes) { if (obj.parent.webserver.meshes[i]._id.replace(/\@/g, 'X').replace(/\$/g, 'X').indexOf(meshIdStart) > 0) { mesh = obj.parent.webserver.meshes[i]; break; } } } - if (mesh == null) { meshNotFoundCount++; parent.debug('mps', 'Mesh not found', username, password); SendUserAuthFail(socket); return -1; } + if (mesh == null) { meshNotFoundCount++; parent.debug('apf', 'Mesh not found', username, password); SendUserAuthFail(socket); return -1; } // If this is a agent-less mesh, use the device guid 3 times as ID. if (mesh.mtype == 1) { @@ -547,14 +550,14 @@ module.exports.CreateApfServer = function (parent, db, args) { socket.addListener("close", function () { socketClosedCount++; - parent.debug('mps', 'APF connection closed'); + parent.debug('apf', 'APF connection closed'); try { delete obj.apfConnections[socket.tag.nodeid]; } catch (e) { } obj.parent.ClearConnectivityState(socket.tag.meshid, socket.tag.nodeid, 8); }); - socket.addListener("error", function () { + socket.addListener("error", function (e) { socketErrorCount++; - //console.log("APF Error: " + socket.remoteAddress); + console.log("APF Error: " + e); }); }