| 1 | /** |
| 2 | * @description WSMAN communication using websocket |
| 3 | * @author Ylian Saint-Hilaire |
| 4 | * @version v0.2.0c |
| 5 | */ |
| 6 | |
| 7 | // Construct a WSMAN communication object |
| 8 | var CreateWsmanComm = function (host, port, user, pass, tls) { |
| 9 | var obj = {}; |
| 10 | obj.PendingAjax = []; // List of pending AJAX calls. When one frees up, another will start. |
| 11 | obj.ActiveAjaxCount = 0; // Number of currently active AJAX calls |
| 12 | obj.MaxActiveAjaxCount = 1; // Maximum number of activate AJAX calls at the same time. |
| 13 | obj.FailAllError = 0; // Set this to non-zero to fail all AJAX calls with that error status, 999 causes responses to be silent. |
| 14 | obj.challengeParams = null; |
| 15 | obj.noncecounter = 1; |
| 16 | obj.authcounter = 0; |
| 17 | obj.socket = null; |
| 18 | obj.socketState = 0; |
| 19 | obj.host = host; |
| 20 | obj.port = port; |
| 21 | obj.user = user; |
| 22 | obj.pass = pass; |
| 23 | obj.tls = tls; |
| 24 | obj.tlsv1only = 1; |
| 25 | obj.cnonce = Math.random().toString(36).substring(7); // Generate a random client nonce |
| 26 | |
| 27 | // Private method |
| 28 | //obj.Debug = function (msg) { console.log(msg); } |
| 29 | |
| 30 | function arrToStr(arr) { return String.fromCharCode.apply(null, arr); } |
| 31 | |
| 32 | // Private method |
| 33 | // pri = priority, if set to 1, the call is high priority and put on top of the stack. |
| 34 | obj.PerformAjax = function (postdata, callback, tag, pri, url, action) { |
| 35 | if (obj.ActiveAjaxCount < obj.MaxActiveAjaxCount && obj.PendingAjax.length == 0) { |
| 36 | // There are no pending AJAX calls, perform the call now. |
| 37 | obj.PerformAjaxEx(postdata, callback, tag, url, action); |
| 38 | } else { |
| 39 | // If this is a high priority call, put this call in front of the array, otherwise put it in the back. |
| 40 | if (pri == 1) { obj.PendingAjax.unshift([postdata, callback, tag, url, action]); } else { obj.PendingAjax.push([postdata, callback, tag, url, action]); } |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | // Private method |
| 45 | obj.PerformNextAjax = function () { |
| 46 | if (obj.ActiveAjaxCount >= obj.MaxActiveAjaxCount || obj.PendingAjax.length == 0) return; |
| 47 | var x = obj.PendingAjax.shift(); |
| 48 | obj.PerformAjaxEx(x[0], x[1], x[2], x[3], x[4]); |
| 49 | obj.PerformNextAjax(); |
| 50 | } |
| 51 | |
| 52 | // Private method |
| 53 | obj.PerformAjaxEx = function (postdata, callback, tag, url, action) { |
| 54 | if (obj.FailAllError != 0) { obj.gotNextMessagesError({ status: obj.FailAllError }, 'error', null, [postdata, callback, tag, url, action]); return; } |
| 55 | if (!postdata) postdata = ""; |
| 56 | //console.log("SEND: " + postdata); // DEBUG |
| 57 | |
| 58 | // We are in a websocket relay environment |
| 59 | obj.ActiveAjaxCount++; |
| 60 | return obj.PerformAjaxExNodeJS(postdata, callback, tag, url, action); |
| 61 | } |
| 62 | |
| 63 | // Websocket relay specific private method |
| 64 | obj.pendingAjaxCall = []; |
| 65 | |
| 66 | // Websocket relay specific private method |
| 67 | obj.PerformAjaxExNodeJS = function (postdata, callback, tag, url, action) { obj.PerformAjaxExNodeJS2(postdata, callback, tag, url, action, 3); } |
| 68 | |
| 69 | // Websocket relay specific private method |
| 70 | obj.PerformAjaxExNodeJS2 = function (postdata, callback, tag, url, action, retry) { |
| 71 | if (retry <= 0 || obj.FailAllError != 0) { |
| 72 | // Too many retry, fail here. |
| 73 | obj.ActiveAjaxCount--; |
| 74 | if (obj.FailAllError != 999) obj.gotNextMessages(null, 'error', { status: ((obj.FailAllError == 0) ? 408 : obj.FailAllError) }, [postdata, callback, tag, url, action]); // 408 is timeout error |
| 75 | obj.PerformNextAjax(); |
| 76 | return; |
| 77 | } |
| 78 | obj.pendingAjaxCall.push([postdata, callback, tag, url, action, retry]); |
| 79 | if (obj.socketState == 0) { obj.xxConnectHttpSocket(); } |
| 80 | else if (obj.socketState == 2) { obj.sendRequest(postdata, url, action); } |
| 81 | } |
| 82 | |
| 83 | // Websocket relay specific private method (Content Length Encoding) |
| 84 | obj.sendRequest = function (postdata, url, action) { |
| 85 | url = url ? url : '/wsman'; |
| 86 | action = action ? action : 'POST'; |
| 87 | var h = action + ' ' + url + ' HTTP/1.1\r\n'; |
| 88 | if (obj.challengeParams != null) { |
| 89 | var response = hex_md5(hex_md5(obj.user + ':' + obj.challengeParams['realm'] + ':' + obj.pass) + ':' + obj.challengeParams['nonce'] + ':' + nonceHex(obj.noncecounter) + ':' + obj.cnonce + ':' + obj.challengeParams['qop'] + ':' + hex_md5(action + ':' + url + ((obj.challengeParams['qop'] == 'auth-int') ? (':' + hex_md5(postdata)) : ''))); |
| 90 | h += 'Authorization: ' + obj.renderDigest({ 'username': obj.user, 'realm': obj.challengeParams['realm'], 'nonce': obj.challengeParams['nonce'], 'uri': url, 'qop': obj.challengeParams['qop'], 'response': response, 'nc': nonceHex(obj.noncecounter++), 'cnonce': obj.cnonce }) + '\r\n'; |
| 91 | } |
| 92 | //h += 'Host: ' + obj.host + ':' + obj.port + '\r\nContent-Length: ' + postdata.length + '\r\n\r\n' + postdata; // Use Content-Length |
| 93 | h += 'Host: ' + obj.host + ':' + obj.port + '\r\nTransfer-Encoding: chunked\r\n\r\n' + postdata.length.toString(16).toUpperCase() + '\r\n' + postdata + '\r\n0\r\n\r\n'; // Use Chunked-Encoding |
| 94 | _Send(h); |
| 95 | //obj.Debug("SEND: " + h); // Display send packet |
| 96 | } |
| 97 | |
| 98 | // Parse the HTTP digest header and return a list of key & values. |
| 99 | obj.parseDigest = function (header) { return correctedQuoteSplit(header.substring(7)).reduce(function (obj, s) { var parts = s.trim().split('='); obj[parts[0]] = parts[1].replace(new RegExp('\"', 'g'), ''); return obj; }, {}) } |
| 100 | |
| 101 | // Split a string on quotes but do not do it when in quotes |
| 102 | function correctedQuoteSplit(str) { return str.split(',').reduce(function (a, c) { if (a.ic) { a.st[a.st.length - 1] += ',' + c } else { a.st.push(c) } if (c.split('"').length % 2 == 0) { a.ic = !a.ic } return a; }, { st: [], ic: false }).st } |
| 103 | function nonceHex(v) { var s = ('00000000' + v.toString(16)); return s.substring(s.length - 8); } |
| 104 | |
| 105 | // Websocket relay specific private method |
| 106 | obj.renderDigest = function (params) { |
| 107 | var paramsnames = []; |
| 108 | for (i in params) { paramsnames.push(i); } |
| 109 | return 'Digest ' + paramsnames.reduce(function (s1, ii) { return s1 + ',' + (((ii == 'nc') || (ii == 'qop')) ? (ii + '=' + params[ii]) : (ii + '="' + params[ii] + '"')); }, '').substring(1); |
| 110 | } |
| 111 | |
| 112 | // Websocket relay specific private method |
| 113 | obj.xxConnectHttpSocket = function () { |
| 114 | //obj.Debug("xxConnectHttpSocket"); |
| 115 | obj.socketParseState = 0; |
| 116 | obj.socketAccumulator = ''; |
| 117 | obj.socketHeader = null; |
| 118 | obj.socketData = ''; |
| 119 | obj.socketState = 1; |
| 120 | obj.socket = new WebSocket(window.location.protocol.replace('http', 'ws') + '//' + window.location.host + window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/')) + '/webrelay.ashx?p=1&host=' + obj.host + '&port=' + obj.port + '&tls=' + obj.tls + '&tlsv1only=' + obj.tlsv1only + ((user == '*') ? '&serverauth=1' : '') + ((typeof pass === 'undefined') ? ('&serverauth=1&user=' + user) : '')); // The "p=1" indicates to the relay that this is a WSMAN session |
| 121 | obj.socket.binaryType = 'arraybuffer'; |
| 122 | obj.socket.onopen = _OnSocketConnected; |
| 123 | obj.socket.onmessage = _OnMessage; |
| 124 | obj.socket.onclose = _OnSocketClosed; |
| 125 | } |
| 126 | |
| 127 | // Websocket relay specific private method |
| 128 | function _OnSocketConnected() { |
| 129 | //obj.Debug("xxOnSocketConnected"); |
| 130 | obj.socketState = 2; |
| 131 | for (i in obj.pendingAjaxCall) { obj.sendRequest(obj.pendingAjaxCall[i][0], obj.pendingAjaxCall[i][3], obj.pendingAjaxCall[i][4]); } |
| 132 | } |
| 133 | |
| 134 | // Websocket relay specific private method |
| 135 | function _OnMessage(e) { |
| 136 | //obj.Debug("_OnSocketData (" + data.byteLength + "): " + data); |
| 137 | obj.socketAccumulator += arrToStr(new Uint8Array(e.data)); |
| 138 | while (true) { |
| 139 | if (obj.socketParseState == 0) { |
| 140 | var headersize = obj.socketAccumulator.indexOf('\r\n\r\n'); |
| 141 | if (headersize < 0) return; |
| 142 | //obj.Debug(obj.socketAccumulator.substring(0, headersize)); // Display received HTTP header |
| 143 | obj.socketHeader = obj.socketAccumulator.substring(0, headersize).split('\r\n'); |
| 144 | if (obj.amtVersion == null) { for (var i in obj.socketHeader) { if (obj.socketHeader[i].indexOf('Server: Intel(R) Active Management Technology ') == 0) { obj.amtVersion = obj.socketHeader[i].substring(46); } } } |
| 145 | obj.socketAccumulator = obj.socketAccumulator.substring(headersize + 4); |
| 146 | obj.socketParseState = 1; |
| 147 | obj.socketData = ''; |
| 148 | obj.socketXHeader = { Directive: obj.socketHeader[0].split(' ') }; |
| 149 | for (i in obj.socketHeader) { |
| 150 | if (i != 0) { |
| 151 | var x2 = obj.socketHeader[i].indexOf(':'); |
| 152 | obj.socketXHeader[obj.socketHeader[i].substring(0, x2).toLowerCase()] = obj.socketHeader[i].substring(x2 + 2); |
| 153 | } |
| 154 | } |
| 155 | } |
| 156 | if (obj.socketParseState == 1) { |
| 157 | var csize = -1; |
| 158 | if ((obj.socketXHeader['connection'] != undefined) && (obj.socketXHeader['connection'].toLowerCase() == 'close') && ((obj.socketXHeader["transfer-encoding"] == undefined) || (obj.socketXHeader["transfer-encoding"].toLowerCase() != 'chunked'))) { |
| 159 | // The body ends with a close, in this case, we will only process the header |
| 160 | csize = 0; |
| 161 | } else if (obj.socketXHeader['content-length'] != undefined) { |
| 162 | // The body length is specified by the content-length |
| 163 | csize = parseInt(obj.socketXHeader['content-length']); |
| 164 | if (obj.socketAccumulator.length < csize) return; |
| 165 | var data = obj.socketAccumulator.substring(0, csize); |
| 166 | obj.socketAccumulator = obj.socketAccumulator.substring(csize); |
| 167 | obj.socketData = data; |
| 168 | csize = 0; |
| 169 | } else { |
| 170 | // The body is chunked |
| 171 | var clen = obj.socketAccumulator.indexOf("\r\n"); |
| 172 | if (clen < 0) return; // Chunk length not found, exit now and get more data. |
| 173 | // Chunk length if found, lets see if we can get the data. |
| 174 | csize = parseInt(obj.socketAccumulator.substring(0, clen), 16); |
| 175 | if (isNaN(csize)) { if (obj.websocket) { obj.websocket.close(); } return; } // Critical error, close the socket and exit. |
| 176 | if (obj.socketAccumulator.length < clen + 2 + csize + 2) return; |
| 177 | // We got a chunk with all of the data, handle the chunck now. |
| 178 | var data = obj.socketAccumulator.substring(clen + 2, clen + 2 + csize); |
| 179 | obj.socketAccumulator = obj.socketAccumulator.substring(clen + 2 + csize + 2); |
| 180 | obj.socketData += data; |
| 181 | } |
| 182 | if (csize == 0) { |
| 183 | //obj.Debug("_OnSocketData DONE: (" + obj.socketData.length + "): " + obj.socketData); |
| 184 | _ProcessHttpResponse(obj.socketXHeader, obj.socketData); |
| 185 | obj.socketParseState = 0; |
| 186 | obj.socketHeader = null; |
| 187 | } |
| 188 | } |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | // Websocket relay specific private method |
| 193 | function _ProcessHttpResponse(header, data) { |
| 194 | //obj.Debug("_ProcessHttpResponse: " + header.Directive[1]); |
| 195 | |
| 196 | var s = parseInt(header.Directive[1]); |
| 197 | if (isNaN(s)) s = 602; |
| 198 | if (s == 401 && ++(obj.authcounter) < 3) { |
| 199 | obj.challengeParams = obj.parseDigest(header['www-authenticate']); // Set the digest parameters, after this, the socket will close and we will auto-retry |
| 200 | if (obj.challengeParams['qop'] != null) { |
| 201 | var qopList = obj.challengeParams['qop'].split(','); |
| 202 | for (var i in qopList) { qopList[i] = qopList[i].trim(); } |
| 203 | if (qopList.indexOf('auth-int') >= 0) { obj.challengeParams['qop'] = 'auth-int'; } else { obj.challengeParams['qop'] = 'auth'; } |
| 204 | } |
| 205 | } else { |
| 206 | var r = obj.pendingAjaxCall.shift(); |
| 207 | // if (s != 200) { obj.Debug("Error, status=" + s + "\r\n\r\nreq=" + r[0] + "\r\n\r\nresp=" + data); } // Debug: Display the request & response if something did not work. |
| 208 | obj.authcounter = 0; |
| 209 | obj.ActiveAjaxCount--; |
| 210 | obj.gotNextMessages(data, 'success', { status: s }, r); |
| 211 | obj.PerformNextAjax(); |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | // Websocket relay specific private method |
| 216 | function _OnSocketClosed(data) { |
| 217 | //obj.Debug("_OnSocketClosed"); |
| 218 | obj.socketState = 0; |
| 219 | if (obj.socket != null) { obj.socket.close(); obj.socket = null; } |
| 220 | if (obj.pendingAjaxCall.length > 0) { |
| 221 | var r = obj.pendingAjaxCall.shift(); |
| 222 | var retry = r[5]; |
| 223 | obj.PerformAjaxExNodeJS2(r[0], r[1], r[2], r[3], r[4], --retry); |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | // Websocket relay specific private method |
| 228 | function _Send(x) { |
| 229 | //console.log("SEND: " + x); // DEBUG |
| 230 | if (obj.socketState == 2 && obj.socket != null && obj.socket.readyState == WebSocket.OPEN) { |
| 231 | var b = new Uint8Array(x.length); |
| 232 | for (var i = 0; i < x.length; ++i) { b[i] = x.charCodeAt(i); } |
| 233 | try { obj.socket.send(b.buffer); } catch (e) { } |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | // Private method |
| 238 | obj.gotNextMessages = function (data, status, request, callArgs) { |
| 239 | if (obj.FailAllError == 999) return; |
| 240 | if (obj.FailAllError != 0) { callArgs[1](null, obj.FailAllError, callArgs[2]); return; } |
| 241 | if (request.status != 200) { callArgs[1](null, request.status, callArgs[2]); return; } |
| 242 | callArgs[1](data, 200, callArgs[2]); |
| 243 | } |
| 244 | |
| 245 | // Private method |
| 246 | obj.gotNextMessagesError = function (request, status, errorThrown, callArgs) { |
| 247 | if (obj.FailAllError == 999) return; |
| 248 | if (obj.FailAllError != 0) { callArgs[1](null, obj.FailAllError, callArgs[2]); return; } |
| 249 | callArgs[1](obj, null, { Header: { HttpError: request.status } }, request.status, callArgs[2]); |
| 250 | } |
| 251 | |
| 252 | // Cancel all pending queries with given status |
| 253 | obj.CancelAllQueries = function (s) { |
| 254 | while (obj.PendingAjax.length > 0) { var x = obj.PendingAjax.shift(); x[1](null, s, x[2]); } |
| 255 | if (obj.websocket != null) { obj.websocket.close(); obj.websocket = null; obj.socketState = 0; } |
| 256 | } |
| 257 | |
| 258 | return obj; |
| 259 | } |
| 260 |