MeshCentral/agents/modules_meshcore/apfclient.js
2020-10-09 15:44:09 -07:00

460 lines
20 KiB
JavaScript

/**
* @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) {
if ((args.clientuuid == null) || (args.clientuuid.length != 36)) return null; // Require a UUID if this exact length
var obj = {};
obj.parent = parent;
obj.args = args;
obj.http = require('http');
obj.net = require('net');
obj.forwardClient = null;
obj.downlinks = {};
obj.pfwd_idx = 0;
obj.timer = null; // Keep alive timer
// 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; };
function char2hex(i) { return (i + 0x100).toString(16).substr(-2).toUpperCase(); }; // Convert decimal to hex
function rstr2hex(input) { var r = '', i; for (i = 0; i < input.length; i++) { r += char2hex(input.charCodeAt(i)); } return r; }; // Convert a raw string to a hex string
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); } }
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) + 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; }
// 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);
// Intel AMT forwarded port list for non-TLS mode
//var pfwd_ports = [16992, 623, 16994, 5900];
var pfwd_ports = [ 16992, 16993 ];
// 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,
MESH_CONNECTION_TYPE: 250 // This is a Mesh specific command that instructs the server of the connection type: 1 = Relay, 2 = LMS.
}
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;
if (typeof obj.args.conntype == 'number') { SendConnectionType(obj.forwardClient.ws, obj.args.conntype); }
SendProtocolVersion(obj.forwardClient.ws, obj.args.clientuuid);
SendServiceRequest(obj.forwardClient.ws, 'auth@amt.intel.com');
}
function SendConnectionType(socket, type) {
socket.write(String.fromCharCode(APFProtocol.MESH_CONNECTION_TYPE) + IntToStr(type));
Debug("APF: Send connection type " + type);
}
function SendProtocolVersion(socket, uuid) {
var data = String.fromCharCode(APFProtocol.PROTOCOLVERSION) + IntToStr(1) + IntToStr(0) + IntToStr(0) + hex2rstr(strToGuid(uuid)) + 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
var chan = obj.net.createConnection({ host: obj.args.clientaddress, port: p_res.target_port }, function () {
//require('MeshAgent').SendCommand({ action: 'msg', type: 'console', value: "CHANNEL_OPEN-open" });
// obj.downlinks[p_res.sender_chan].setEncoding('binary');//assume everything is binary, not interpreting
SendChannelOpenConfirm(socket.ws, p_res);
});
// Setup flow control
chan.maxInWindow = p_res.window_size; // Oddly, we are using the same window size as the other side.
chan.curInWindow = 0;
chan.on('data', function (ddata) {
// Relay data to fordwardclient
// TODO: Implement flow control
SendChannelData(socket.ws, p_res.sender_chan, ddata.length, ddata);
});
chan.on('error', function (e) {
Debug("Downlink connection error: " + e);
});
chan.on('end', function () {
var chan = obj.downlinks[p_res.sender_chan];
if (chan != null) {
try {
Debug("Socket ends.");
SendChannelClose(socket.ws, p_res.sender_chan);
chan.xclosed = 1;
// 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);
}
}
});
obj.downlinks[p_res.sender_chan] = chan;
} else {
// Not a supported port, fail the connection
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);
var chan = obj.downlinks[rcpt_chan];
if ((chan != null) && (chan.xclosed !== 1)) {
SendChannelClose(socket.ws, rcpt_chan);
try { obj.downlinks[rcpt_chan].end(); } catch (e) { }
delete obj.downlinks[rcpt_chan];
}
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);
var chan = obj.downlinks[rcpt_chan];
if (chan != null) {
chan.curInWindow += chan_data_len;
try {
chan.write(chan_data, 'binary', function () {
Debug("Write completed.");
// If the incoming window is over half used, send an adjust.
if (this.curInWindow > (this.maxInWindow / 2)) { SendChannelWindowAdjust(socket.ws, rcpt_chan, this.curInWindow); this.curInWindow = 0; }
});
} 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 = { cmd: APFProtocol.CHANNEL_OPEN };
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;