agent-redir-ws-0.1.1.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. /**
  2. * @description Mesh Agent Transport Module - using websocket relay
  3. * @author Ylian Saint-Hilaire
  4. * @version v0.0.1f
  5. */
  6. // Construct a MeshServer agent direction object
  7. var CreateAgentRedirect = function (meshserver, module, serverPublicNamePort, authCookie, rauthCookie, domainUrl) {
  8. var obj = {};
  9. obj.m = module; // This is the inner module (Terminal or Desktop)
  10. module.parent = obj;
  11. obj.meshserver = meshserver;
  12. obj.authCookie = authCookie;
  13. obj.rauthCookie = rauthCookie;
  14. obj.State = 0; // 0 = Disconnected, 1 = Connected, 2 = Connected to server, 3 = End-to-end connection.
  15. obj.nodeid = null;
  16. obj.options = null;
  17. obj.socket = null;
  18. obj.connectstate = -1;
  19. obj.tunnelid = Math.random().toString(36).substring(2); // Generate a random client tunnel id
  20. obj.protocol = module.protocol; // 1 = SOL, 2 = KVM, 3 = IDER, 4 = Files, 5 = FileTransfer
  21. obj.onStateChanged = null;
  22. obj.ctrlMsgAllowed = true;
  23. obj.attemptWebRTC = false;
  24. obj.webRtcActive = false;
  25. obj.webrtcconfig = null;
  26. obj.webSwitchOk = false;
  27. obj.webchannel = null;
  28. obj.webrtc = null;
  29. obj.debugmode = 0;
  30. obj.serverIsRecording = false;
  31. obj.urlname = 'meshrelay.ashx';
  32. obj.latency = { lastSend: null, current: -1, callback: null };
  33. if (domainUrl == null) { domainUrl = '/'; }
  34. // Console Message
  35. obj.consoleMessage = null;
  36. obj.onConsoleMessageChange = null;
  37. // Session Metadata
  38. obj.metadata = null;
  39. obj.onMetadataChange = null;
  40. // Private method
  41. //obj.debug = function (msg) { console.log(msg); }
  42. // Display websocket or webrtc data to the console
  43. function logData(e, name) {
  44. if (typeof e.data == 'object') {
  45. var view = new Uint8Array(e.data), cmd = (view[0] << 8) + view[1], cmdsize = (view[2] << 8) + view[3];
  46. console.log(name + ' binary data', cmd, cmdsize, e.data.byteLength, buf2hex(e.data).substring(0, 24));
  47. } else if (typeof e.data == 'string') {
  48. console.log(name + ' string data', e.data.length, e.data);
  49. } else {
  50. console.log(name + ' unknown data', e.data);
  51. }
  52. }
  53. obj.Start = function (nodeid) {
  54. var url2, url = window.location.protocol.replace('http', 'ws') + '//' + window.location.host + window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/')) + '/' + obj.urlname + '?browser=1&p=' + obj.protocol + (nodeid?('&nodeid=' + nodeid):'') + '&id=' + obj.tunnelid;
  55. //if (serverPublicNamePort) { url2 = window.location.protocol.replace('http', 'ws') + '//' + serverPublicNamePort + '/meshrelay.ashx?id=' + obj.tunnelid; } else { url2 = url; }
  56. if ((authCookie != null) && (authCookie != '')) { url += '&auth=' + authCookie; }
  57. if ((urlargs != null) && (urlargs.slowrelay != null)) { url += '&slowrelay=' + urlargs.slowrelay; }
  58. obj.nodeid = nodeid;
  59. obj.connectstate = 0;
  60. obj.socket = new WebSocket(url);
  61. obj.socket.binaryType = 'arraybuffer';
  62. obj.socket.onopen = obj.xxOnSocketConnected;
  63. obj.socket.onmessage = obj.xxOnMessage;
  64. //obj.socket.onmessage = function (e) { logData(e, 'WebSocket'); obj.xxOnMessage(e); }
  65. obj.socket.onerror = function (e) { /* console.error(e); */ }
  66. obj.socket.onclose = obj.xxOnSocketClosed;
  67. obj.xxStateChange(1);
  68. if (obj.meshserver != null) {
  69. var rurl = '*' + domainUrl + 'meshrelay.ashx?p=' + obj.protocol + '&nodeid=' + nodeid + '&id=' + obj.tunnelid;
  70. if ((rauthCookie != null) && (rauthCookie != '')) { rurl += ('&rauth=' + rauthCookie); }
  71. obj.meshserver.send({ action: 'msg', type: 'tunnel', nodeid: obj.nodeid, value: rurl, usage: obj.protocol });
  72. //obj.debug('Agent Redir Start: ' + url);
  73. }
  74. }
  75. obj.xxOnSocketConnected = function () {
  76. if (obj.debugmode == 1) { console.log('onSocketConnected'); }
  77. //obj.debug('Agent Redir Socket Connected');
  78. if (!obj.latency.lastSend){
  79. obj.latency.lastSend = setInterval(function(){
  80. if (obj.latency.current == -1) {
  81. clearInterval(obj.latency.lastSend);
  82. obj.latency.lastSend = null;
  83. } else {
  84. obj.sendCtrlMsg(JSON.stringify({ctrlChannel:102938,type:"rtt",time:(new Date().getTime())}));
  85. }
  86. }, 10000);
  87. }
  88. obj.sendCtrlMsg(JSON.stringify({ctrlChannel:102938,type:"rtt",time:(new Date().getTime())}));
  89. obj.xxStateChange(2);
  90. }
  91. // Called to pass websocket control messages
  92. obj.xxOnControlCommand = function (msg) {
  93. var controlMsg;
  94. try { controlMsg = JSON.parse(msg); } catch (e) { return; }
  95. if (controlMsg.ctrlChannel != '102938') { if (obj.m.ProcessData) { obj.m.ProcessData(msg); } else { console.log(msg); } return; }
  96. if ((typeof args != 'undefined') && args.redirtrace) { console.log('RedirRecv', controlMsg); }
  97. if (controlMsg.type == 'console') {
  98. obj.setConsoleMessage(controlMsg.msg, controlMsg.msgid, controlMsg.msgargs, controlMsg.timeout);
  99. } else if (controlMsg.type == 'metadata') {
  100. obj.metadata = controlMsg;
  101. if (obj.onMetadataChange) obj.onMetadataChange(obj.metadata);
  102. } else if ((controlMsg.type == 'rtt') && (typeof controlMsg.time == 'number')) {
  103. obj.latency.current = (new Date().getTime()) - controlMsg.time;
  104. if (obj.latency.callback != null) { obj.latency.callback(obj.latency.current); }
  105. } else if (obj.webrtc != null) {
  106. if (controlMsg.type == 'answer') {
  107. obj.webrtc.setRemoteDescription(new RTCSessionDescription(controlMsg), function () { /*console.log('WebRTC remote ok');*/ }, obj.xxCloseWebRTC);
  108. } else if (controlMsg.type == 'webrtc0') {
  109. obj.webSwitchOk = true; // Other side is ready for switch over
  110. performWebRtcSwitch();
  111. } else if (controlMsg.type == 'webrtc1') {
  112. obj.sendCtrlMsg('{"ctrlChannel":"102938","type":"webrtc2"}'); // Confirm we got end of data marker, indicates data will no longer be received on websocket.
  113. } else if (controlMsg.type == 'webrtc2') {
  114. // TODO: Resume/Start sending data over WebRTC
  115. }
  116. } else if (controlMsg.type == 'ping') { // if we get a ping, respond with a pong.
  117. obj.sendCtrlMsg('{"ctrlChannel":"102938","type":"pong"}');
  118. }
  119. }
  120. // Set the console message
  121. obj.setConsoleMessage = function (str, id, args, timeout) {
  122. if (obj.consoleMessage == str) return;
  123. obj.consoleMessage = str;
  124. obj.consoleMessageId = id;
  125. obj.consoleMessageArgs = args;
  126. obj.consoleMessageTimeout = timeout;
  127. if (obj.onConsoleMessageChange) { obj.onConsoleMessageChange(obj, obj.consoleMessage, obj.consoleMessageId); }
  128. }
  129. obj.sendCtrlMsg = function (x) { if (obj.ctrlMsgAllowed == true) { if ((typeof args != 'undefined') && args.redirtrace) { console.log('RedirSend', typeof x, x); } try { obj.socket.send(x); } catch (ex) { } } }
  130. function performWebRtcSwitch() {
  131. if ((obj.webSwitchOk == true) && (obj.webRtcActive == true)) {
  132. obj.latency.current = -1; // RTT will no longer be calculated when WebRTC is enabled
  133. obj.sendCtrlMsg('{"ctrlChannel":"102938","type":"webrtc0"}'); // Indicate to the meshagent that it can start traffic switchover
  134. obj.sendCtrlMsg('{"ctrlChannel":"102938","type":"webrtc1"}'); // Indicate to the meshagent that data traffic will no longer be sent over websocket.
  135. // TODO: Hold/Stop sending data over websocket
  136. if (obj.onStateChanged != null) { obj.onStateChanged(obj, obj.State); }
  137. }
  138. }
  139. obj.xxOnMessage = function (e) {
  140. //console.log('Recv', e.data, e.data.byteLength, obj.State);
  141. if (obj.State < 3) {
  142. if ((e.data == 'c') || (e.data == 'cr')) {
  143. if (e.data == 'cr') { obj.serverIsRecording = true; }
  144. if (obj.options != null) { delete obj.options.action; obj.options.type = 'options'; try { obj.sendCtrlMsg(JSON.stringify(obj.options)); } catch (ex) { } }
  145. try { obj.socket.send(obj.protocol); } catch (ex) { }
  146. obj.xxStateChange(3);
  147. if (obj.attemptWebRTC == true) {
  148. // Try to get WebRTC setup
  149. var configuration = obj.webrtcconfig; //{ "iceServers": [ { 'urls': 'stun:stun.cloudflare.com:3478' }, { 'urls': 'stun:stun.l.google.com:19302' } ] };
  150. if (typeof RTCPeerConnection !== 'undefined') { obj.webrtc = new RTCPeerConnection(configuration); }
  151. else if (typeof webkitRTCPeerConnection !== 'undefined') { obj.webrtc = new webkitRTCPeerConnection(configuration); }
  152. if ((obj.webrtc != null) && (obj.webrtc.createDataChannel)) {
  153. obj.webchannel = obj.webrtc.createDataChannel('DataChannel', {}); // { ordered: false, maxRetransmits: 2 }
  154. obj.webchannel.binaryType = 'arraybuffer';
  155. obj.webchannel.onmessage = obj.xxOnMessage;
  156. //obj.webchannel.onmessage = function (e) { logData(e, 'WebRTC'); obj.xxOnMessage(e); }
  157. obj.webchannel.onopen = function () { obj.webRtcActive = true; performWebRtcSwitch(); };
  158. obj.webchannel.onclose = function (event) { if (obj.webRtcActive) { obj.Stop(); } }
  159. obj.webrtc.onicecandidate = function (e) {
  160. if (e.candidate == null) {
  161. try { obj.sendCtrlMsg(JSON.stringify(obj.webrtcoffer)); } catch (ex) { } // End of candidates, send the offer
  162. } else {
  163. obj.webrtcoffer.sdp += ('a=' + e.candidate.candidate + '\r\n'); // New candidate, add it to the SDP
  164. }
  165. }
  166. obj.webrtc.oniceconnectionstatechange = function () {
  167. if (obj.webrtc != null) {
  168. if (obj.webrtc.iceConnectionState == 'disconnected') { if (obj.webRtcActive == true) { obj.Stop(); } else { obj.xxCloseWebRTC(); } }
  169. else if (obj.webrtc.iceConnectionState == 'failed') { obj.xxCloseWebRTC(); }
  170. }
  171. }
  172. obj.webrtc.createOffer(function (offer) {
  173. // Got the offer
  174. obj.webrtcoffer = offer;
  175. obj.webrtc.setLocalDescription(offer, function () { /*console.log('WebRTC local ok');*/ }, obj.xxCloseWebRTC);
  176. }, obj.xxCloseWebRTC, { mandatory: { OfferToReceiveAudio: false, OfferToReceiveVideo: false } });
  177. }
  178. }
  179. return;
  180. }
  181. }
  182. // Control messages, most likely WebRTC setup
  183. //console.log('New data', e.data.byteLength);
  184. if (typeof e.data == 'string') {
  185. if (e.data[0] == '~') { obj.m.ProcessData(e.data); } else { obj.xxOnControlCommand(e.data); }
  186. } else {
  187. // Send the data to the module
  188. if (obj.m.ProcessBinaryCommand) {
  189. // If only 1 byte
  190. if ((cmdAccLen == 0) && (e.data.byteLength < 4)) return; // Ignore any commands less than 4 bytes.
  191. // Send as Binary Command
  192. if (cmdAccLen != 0) {
  193. // Accumulator is active
  194. var view = new Uint8Array(e.data);
  195. cmdAcc.push(view);
  196. cmdAccLen += view.byteLength;
  197. //console.log('Accumulating', cmdAccLen);
  198. if (cmdAccCmdSize <= cmdAccLen) {
  199. var tmp = new Uint8Array(cmdAccLen), tmpPtr = 0;
  200. for (var i in cmdAcc) { tmp.set(cmdAcc[i], tmpPtr); tmpPtr += cmdAcc[i].byteLength; }
  201. //console.log('AccumulatorCompleted');
  202. obj.m.ProcessBinaryCommand(cmdAccCmd, cmdAccCmdSize, tmp);
  203. cmdAccCmd = 0, cmdAccCmdSize = 0, cmdAccLen = 0, cmdAcc = [];
  204. }
  205. } else {
  206. // Accumulator is not active
  207. var view = new Uint8Array(e.data), cmd = (view[0] << 8) + view[1], cmdsize = (view[2] << 8) + view[3];
  208. if ((cmd == 27) && (cmdsize == 8)) { cmd = (view[8] << 8) + view[9]; cmdsize = (view[5] << 16) + (view[6] << 8) + view[7]; view = view.slice(8); }
  209. //console.log(cmdsize, view.byteLength);
  210. if (cmdsize != view.byteLength) {
  211. //console.log('AccumulatorRequired', cmd, cmdsize, view.byteLength);
  212. cmdAccCmd = cmd; cmdAccCmdSize = cmdsize; cmdAccLen = view.byteLength, cmdAcc = [view];
  213. } else {
  214. obj.m.ProcessBinaryCommand(cmd, cmdsize, view);
  215. }
  216. }
  217. } else if (obj.m.ProcessBinaryData) {
  218. // Send as Binary
  219. obj.m.ProcessBinaryData(new Uint8Array(e.data));
  220. } else {
  221. // Send as Text
  222. if (e.data.byteLength < 16000) { // Process small data block
  223. obj.m.ProcessData(String.fromCharCode.apply(null, new Uint8Array(e.data))); // This will stack overflow on Chrome with 100k+ blocks.
  224. } else { // Process large data block
  225. var bb = new Blob([new Uint8Array(e.data)]), f = new FileReader();
  226. f.onload = function (e) { obj.m.ProcessData(e.target.result); };
  227. f.readAsBinaryString(bb);
  228. }
  229. }
  230. }
  231. };
  232. // Command accumulator, this is used for WebRTC fragmentation
  233. var cmdAccCmd = 0, cmdAccCmdSize = 0, cmdAccLen = 0, cmdAcc = [];
  234. obj.sendText = function (x) {
  235. if (typeof x != 'string') { x = JSON.stringify(x); } // Turn into a string if needed
  236. obj.send(encode_utf8(x)); // Encode UTF8 correctly
  237. }
  238. obj.send = function (x) {
  239. //obj.debug('Agent Redir Send(' + obj.webRtcActive + ', ' + x.length + '): ' + rstr2hex(x));
  240. //console.log('Agent Redir Send(' + obj.webRtcActive + ', ' + x.length + '): ' + ((typeof x == 'string')?x:rstr2hex(x)));
  241. if ((typeof args != 'undefined') && args.redirtrace) { console.log('RedirSend', typeof x, x.length, (x[0] == '{') ? x : rstr2hex(x).substring(0, 64)); }
  242. try {
  243. if (obj.socket != null && obj.socket.readyState == WebSocket.OPEN) {
  244. if (typeof x == 'string') {
  245. if (obj.debugmode == 1) {
  246. var b = new Uint8Array(x.length), c = [];
  247. for (var i = 0; i < x.length; ++i) { b[i] = x.charCodeAt(i); c.push(x.charCodeAt(i)); }
  248. if (obj.webRtcActive == true) { obj.webchannel.send(b.buffer); } else { obj.socket.send(b.buffer); }
  249. //console.log('Send', c);
  250. } else {
  251. var b = new Uint8Array(x.length);
  252. for (var i = 0; i < x.length; ++i) { b[i] = x.charCodeAt(i); }
  253. if (obj.webRtcActive == true) { obj.webchannel.send(b.buffer); } else { obj.socket.send(b.buffer); }
  254. }
  255. } else {
  256. //if (obj.debugmode == 1) { console.log('Send', x); }
  257. if (obj.webRtcActive == true) { obj.webchannel.send(x); } else { obj.socket.send(x); }
  258. }
  259. }
  260. } catch (ex) { }
  261. }
  262. obj.xxOnSocketClosed = function () {
  263. //obj.debug('Agent Redir Socket Closed');
  264. //if (obj.debugmode == 1) { console.log('onSocketClosed'); }
  265. obj.Stop(1);
  266. }
  267. obj.xxStateChange = function(newstate) {
  268. if (obj.State == newstate) return;
  269. obj.State = newstate;
  270. obj.m.xxStateChange(obj.State);
  271. if (obj.onStateChanged != null) obj.onStateChanged(obj, obj.State);
  272. }
  273. // Close the WebRTC connection, should be called if a problem occurs during WebRTC setup.
  274. obj.xxCloseWebRTC = function () {
  275. if (obj.webchannel != null) { try { obj.webchannel.close(); } catch (e) { } obj.webchannel = null; }
  276. if (obj.webrtc != null) { try { obj.webrtc.close(); } catch (e) { } obj.webrtc = null; }
  277. obj.webRtcActive = false;
  278. }
  279. obj.Stop = function (x) {
  280. if (obj.debugmode == 1) { console.log('stop', x); }
  281. // Clean up WebRTC
  282. obj.xxCloseWebRTC();
  283. // clear RTT timer
  284. obj.latency.current = -1;
  285. //obj.debug('Agent Redir Socket Stopped');
  286. obj.connectstate = -1;
  287. if (obj.socket != null) {
  288. try { if (obj.socket.readyState == 1) { obj.sendCtrlMsg('{"ctrlChannel":"102938","type":"close"}'); } } catch (ex) { } // If connected, send the close command
  289. try { if (obj.socket.readyState <= 1) { obj.socket.close(); } } catch (ex) { } // If connecting or connected, close the websocket
  290. obj.socket = null;
  291. }
  292. obj.xxStateChange(0);
  293. }
  294. // Buffer is an ArrayBuffer
  295. function buf2hex(buffer) { return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join(''); }
  296. return obj;
  297. }