UNPKG

alexa-remote2

Version:

Remote Control for amazon echo devices

863 lines (767 loc) 38.8 kB
const WebSocket = require('ws'); const EventEmitter = require('events'); const crypto = require('crypto'); class AlexaWsMqtt extends EventEmitter { constructor(options, cookie, macDms) { super(); this._options = options; this.stop = false; let serialArr = null; this.cookie = cookie; this.macDms = macDms; if (cookie) serialArr = cookie.match(/ubid-[a-z]+=([^;]+);/); if (!serialArr || !serialArr[1]) { this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Cookie incomplete : ' + JSON.stringify(serialArr)); return undefined; } this.accountSerial = serialArr[1]; this.websocket = null; this.pingPongInterval = null; this.errorRetryCounter = 0; this.reconnectTimeout = null; this.pongTimeout = null; this.initTimeout = null; this.connectionActive = false; this.messageId = Math.floor(1E9 * Math.random()); } isConnected() { return this.connectionActive; } connectType1() { const urlTime = Date.now(); let amazonPage = '.' + this._options.amazonPage; if (amazonPage === '.amazon.com') amazonPage = '-js.amazon.com'; // Special Handling for US! const url = `wss://dp-gw-na${amazonPage}/?x-amz-device-type=ALEGCNGL9K0HM&x-amz-device-serial=${this.accountSerial}-${urlTime}`; this.websocket = new WebSocket(url, [], { 'perMessageDeflate': true, 'protocolVersion': 13, 'timeout': 5000, 'headers': { 'Connection': 'keep-alive, Upgrade', 'Upgrade': 'websocket', 'Host': 'dp-gw-na.' + this._options.amazonPage, 'Origin': 'https://alexa.' + this._options.amazonPage, 'Pragma': 'no-cache', 'Cache-Control': 'no-cache', //'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', //'Accept-Language': 'de,en-US;q=0.7,en;q=0.3', //'Sec-WebSocket-Key': 'aV/ud2q+G4pTtOhlt/Amww==', //'Sec-WebSocket-Extensions': 'permessage-deflate', // 'x-webkit-deflate-frame', //'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15G77 PitanguiBridge/2.2.219248.0-[HARDWARE=iPhone10_4][SOFTWARE=11.4.1]', 'Cookie': this.cookie, } } ); return url; } /** * @param method The http request method (GET, POST, DELETE, ...). * @param path The requested http url path and query. * @param body The http message body. * @return {string} */ createRequestSignature(method, path, body) { /* date = datetime.utcnow().isoformat("T") + "Z" body = body.decode("utf-8") data = f"{method}\n{path}\n{date}\n{body}\n{adp_token}" key = rsa.PrivateKey.load_pkcs1(private_key.encode("utf-8")) cipher = rsa.pkcs1.sign(data.encode(), key, "SHA-256") signed_encoded = base64.b64encode(cipher) signature = f"{signed_encoded.decode()}:{date}" return { "x-adp-token": adp_token, "x-adp-alg": "SHA256withRSA:1.0", "x-adp-signature": signature } <base64>:2021-10-08T23:49:33Z */ const now = new Date().toISOString(); const sign = crypto.createSign('SHA256'); sign.write(method + '\n'); sign.write(path + '\n'); sign.write(now + '\n'); sign.write(body.toString('utf-8') + '\n'); sign.write(this.macDms.adp_token); sign.end(); const privateKey = '-----BEGIN PRIVATE KEY-----\n' + this.macDms.device_private_key + '\n-----END PRIVATE KEY-----'; return `${sign.sign(privateKey, 'base64')}:${now}`; } connectType2() { let amazonPage = '.' + this._options.amazonPage; if (amazonPage === '.amazon.com') amazonPage = '-js.amazon.com'; // Special Handling for US! const url = `wss://dp-gw-na${amazonPage}/tcomm/`; this.websocket = new WebSocket(url, [], { 'perMessageDeflate': true, 'protocolVersion': 13, 'timeout': 5000, 'headers': { 'Connection': 'keep-alive, Upgrade', 'Upgrade': 'websocket', 'Host': 'dp-gw-na.' + this._options.amazonPage, 'Origin': 'https://alexa.' + this._options.amazonPage, 'Pragma': 'no-cache', 'Cache-Control': 'no-cache', //'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', //'Accept-Language': 'de,en-US;q=0.7,en;q=0.3', //'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15G77 PitanguiBridge/2.2.219248.0-[HARDWARE=iPhone10_4][SOFTWARE=11.4.1]', 'Cookie': this.cookie, 'x-dp-comm-tuning': 'A:F;A:H', 'x-dp-reason': 'ClientInitiated;1', 'x-dp-tcomm-purpose': 'Regular', //'x-dp-deviceVersion': 'motorola/osprey_reteu_2gb/osprey_u2:6.0.1/MPI24.107-55/33:user/release-keys', //'x-dp-networkType': 'WIFI', //'x-dp-tcomm-versionCode': '894920010', //'x-dp-oui': 'dca632', 'x-dp-obfuscatedBssid': '-2019514039', 'x-dp-tcomm-versionName': '2.2.443692.0', 'x-adp-signature': this.createRequestSignature('GET', '/tcomm/', ''), 'x-adp-token': this.macDms.adp_token, 'x-adp-alg': 'SHA256WithRSA:1.0', } } ); return url; } sendWs(data) { return new Promise(resolve => { if (!this.websocket) { resolve(); return; } this.websocket.send(data, () => resolve()); }); } wait(delay) { return new Promise(resolve => { setTimeout(() => resolve(), delay); }); } connect() { let url; try { if (this._options.usePushConnectType !== undefined) { this._options.logger && this._options.logger(`Alexa-Remote WS-MQTT: Try to initialize push connection with type ${this._options.usePushConnectType}`); if (this._options.usePushConnectType === 1) { url = this.connectType1(); } else if (this._options.usePushConnectType === 2) { url = this.connectType2(); } else { throw new Error(`Invalid push connect type ${this._options.usePushConnectType} specified`); } } else if (!this.macDms || !this.macDms.adp_token || !this.macDms.device_private_key) { this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Try to initialize old style push connection because macDms data missing'); url = this.connectType1(); } else { url = this.connectType2(); } } catch (err) { this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Error on Init ' + err.message); this._options.logger && this._options.logger(err.stack); this.emit('error', err); return; } let msgCounter = 0; this.initTimeout && clearTimeout(this.initTimeout); const onWebsocketClose = (code, reason) => { if (reason) { reason = reason.toString(); } this.websocket = null; this.connectionActive = false; this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Close: ' + code + ': ' + reason); if (this.initTimeout) { clearTimeout(this.initTimeout); this.initTimeout = null; } if (this.pingPongInterval) { clearInterval(this.pingPongInterval); this.pingPongInterval = null; } if (this.pongTimeout) { clearTimeout(this.pongTimeout); this.pongTimeout = null; } if (code === 4001 && reason.startsWith('before - Could not find any')) { // code = 40001, reason = "before - Could not find any vali" this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Cookie invalid!'); this.emit('disconnect', false, 'Cookie invalid'); return; } if (this.stop) return; if (this.errorRetryCounter > 100) { this.emit('disconnect', false, 'Too many failed retries. Check cookie and data'); return; } else { this.errorRetryCounter++; } const retryDelay = Math.min(60, (this.errorRetryCounter * 5) + 5); this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Retry Connection in ' + retryDelay + 's'); this.emit('disconnect', true, 'Retry Connection in ' + retryDelay + 's'); this.reconnectTimeout && clearTimeout(this.reconnectTimeout); this.reconnectTimeout = setTimeout(() => { this.reconnectTimeout = null; this.connect(); }, retryDelay * 1000); }; this.initTimeout = setTimeout(() => { this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Initialization not done within 30s'); try { this.websocket && this.websocket.close(); } catch (err) { //just make sure } if (this.websocket || !this.reconnectTimeout) { // seems no close was emmitted so far?! onWebsocketClose(); } }, 30000); this.websocket.on('open', () => { if (! this.websocket) return; if (this.stop) { this.websocket.close(); return; } this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Open: ' + url); this.connectionActive = false; if (!this.macDms) { // tell Tuning Service that we support "A:H" protocol = AlphaPrococol const msg = Buffer.from('0x99d4f71a 0x0000001d A:HTUNE'); //console.log('SEND: ' + msg.toString('ascii')); this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: A:H Initialization Msg 1 sent'); if (this.websocket.readyState !== 1 /* OPEN */) return; this.websocket.send(msg); } }); this.websocket.on('close', onWebsocketClose); this.websocket.on('error', (error) => { this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Error: ' + error); this.emit('error', error); this.websocket && this.websocket.terminate(); }); this.websocket.on('unexpected-response', () => { this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Unexpected Response'); }); this.websocket.on('message', async (data) => { if (!this.websocket || this.websocket.readyState !== 1 /* OPEN */) { return; } this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Incoming RAW message: ' + data.toString('hex')); const message = this.parseIncomingMessage(data); this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Incoming message: ' + JSON.stringify(message)); if (msgCounter === 0) { // initialization if (message.content.protocolName) { this.protocolName = message.content.protocolName; if (this.protocolName !== 'A:H' && this.protocolName !== 'A:F') { this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Server requests unknown protocol: ' + this.protocolName); } } else { this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Unexpected Response: ' + JSON.stringify(message)); this.protocolName = this.macDms ? 'A:F' : 'A:H'; } this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Detected protocol ' + this.protocolName); let msg; if (this.protocolName === 'A:F') { // A:F msg = Buffer.from('0xfe88bc52 0x0000009c {"protocolName":"A:F","parameters":{"AlphaProtocolHandler.receiveWindowSize":"16","AlphaProtocolHandler.maxFragmentSize":"16000"}}TUNE'); await this.sendWs(msg); this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: A:F Initialization Msg 2 sent: ' + msg.toString('hex')); await this.wait(50); msg = this.encodeGWRegisterAF(); this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: A:F Initialization Msg 3 (Register Connection) sent: ' + msg.toString('hex')); //console.log('SEND: ' + msg.toString('ascii')); await this.sendWs(msg); msgCounter++; } else { // A:H msg = Buffer.from('0xa6f6a951 0x0000009c {"protocolName":"A:H","parameters":{"AlphaProtocolHandler.receiveWindowSize":"16","AlphaProtocolHandler.maxFragmentSize":"16000"}}TUNE'); //console.log('SEND: ' + msg.toString('ascii')); await this.sendWs(msg); await this.wait(50); //msg = new Buffer('MSG 0x00000361 0x0e414e45 f 0x00000001 0xd7c62f29 0x0000009b INI 0x00000003 1.0 0x00000024 ff1c4525-c036-4942-bf6c-a098755ac82f 0x00000164d106ce6b END FABE'); msg = this.encodeGWHandshake(); //console.log('SEND: ' + msg.toString('ascii')); await this.sendWs(msg); this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: A-H Initialization Msg 2+3 sent'); } } if (msgCounter === 1) { if (this.protocolName === 'A:H') { //let msg = new Buffer('MSG 0x00000362 0x0e414e46 f 0x00000001 0xf904b9f5 0x00000109 GWM MSG 0x0000b479 0x0000003b urn:tcomm-endpoint:device:deviceType:0:deviceSerialNumber:0 0x00000041 urn:tcomm-endpoint:service:serviceName:DeeWebsiteMessagingService {"command":"REGISTER_CONNECTION"}FABE'); const msg = this.encodeGWRegisterAH(); this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Initialization Msg 4 (Register Connection) sent'); //console.log('SEND: ' + msg.toString('ascii')); await this.sendWs(msg); } setTimeout(() => { if (!this.websocket || this.websocket.readyState !== 1 /* OPEN */) { return; } //msg = new Buffer('4D53472030783030303030303635203078306534313465343720662030783030303030303031203078626332666262356620307830303030303036322050494E00000000D1098D8CD1098D8C000000070052006500670075006C0061007246414245', 'hex'); // "MSG 0x00000065 0x0e414e47 f 0x00000001 0xbc2fbb5f 0x00000062 PIN" + 30 + "FABE" const msg = this.encodePing(); this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Send First Ping'); //console.log('SEND: ' + msg.toString('hex')); this.websocket.send(msg); this.pingPongInterval = setInterval(() => { if (!this.websocket) return; //let msg = new Buffer('4D53472030783030303030303635203078306534313465343720662030783030303030303031203078626332666262356620307830303030303036322050494E00000000D1098D8CD1098D8C000000070052006500670075006C0061007246414245', 'hex'); // "MSG 0x00000065 0x0e414e47 f 0x00000001 0xbc2fbb5f 0x00000062 PIN" + 30 + "FABE" const msg = this.encodePing(); this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Send Ping'); //console.log('SEND: ' + msg.toString('hex')); this.websocket.send(msg); this.pongTimeout = setTimeout(() => { this.pongTimeout = null; this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: No Pong received after 30s'); this.websocket && this.websocket.close(); }, 30000); }, 180000); }, 100); } msgCounter++; if (msgCounter < 3 || !this.pingPongInterval) return; const incomingMsg = data.toString('ascii'); //if (incomingMsg.includes('PON') && incomingMsg.includes('\u0000R\u0000e\u0000g\u0000u\u0000l\u0000a\u0000r')) { if (message.service === 'FABE' && message.content && message.content.messageType === 'PON' && message.content.payloadData && message.content.payloadData.includes('\u0000R\u0000e\u0000g\u0000u\u0000l\u0000a\u0000r')) { this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Received Pong'); if (this.initTimeout) { clearTimeout(this.initTimeout); this.initTimeout = null; this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Initialization completed'); this.emit('connect'); } if (this.pongTimeout) { clearTimeout(this.pongTimeout); this.pongTimeout = null; } this.connectionActive = true; return; } else if (message.content && message.content.payload) { const command = message.content.payload.command; const payload = message.content.payload.payload; this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Command ' + command + ': ' + JSON.stringify(payload, null, 4)); this.emit('command', command, payload); return; } this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Unknown Data (' + msgCounter + '): ' + incomingMsg); this.emit('unknown', incomingMsg); }); } encodeNumber(val, byteLen) { if (!byteLen) byteLen = 8; let str = val.toString(16); while (str.length < byteLen) str = '0' + str; return '0x' +str; } generateUUID() { const a = []; for (let b = 0; 36 > b; b++) { const c = 'rrrrrrrr-rrrr-4rrr-srrr-rrrrrrrrrrrr'.charAt(b); if ('r' === c || 's' === c) { let d = Math.floor(16 * Math.random()); 's' === c && (d = d & 3 | 8); a.push(d.toString(16)); } else a.push(c); } return a.join(''); } encodeGWHandshake() { //pubrelBuf = new Buffer('MSG 0x00000361 0x0e414e45 f 0x00000001 0xd7c62f29 0x0000009b INI 0x00000003 1.0 0x00000024 ff1c4525-c036-4942-bf6c-a098755ac82f 0x00000164d106ce6b END FABE'); this.messageId++; let msg = 'MSG 0x00000361 '; // Message-type and Channel = GW_HANDSHAKE_CHANNEL; msg += this.encodeNumber(this.messageId) + ' f 0x00000001 '; const idx1 = msg.length; msg += '0x00000000 '; // Checksum! const idx2 = msg.length; msg += '0x0000009b '; // length content msg += 'INI 0x00000003 1.0 0x00000024 '; // content part 1 msg += this.generateUUID(); msg += ' '; msg += this.encodeNumber(Date.now(), 16); msg += ' END FABE'; const completeBuffer = Buffer.from(msg, 'ascii'); const checksum = this.computeChecksum(completeBuffer, idx1, idx2); const checksumBuf = Buffer.from(this.encodeNumber(checksum)); checksumBuf.copy(completeBuffer, 39); return completeBuffer; } encodeGWRegisterAH() { //pubrelBuf = new Buffer('MSG 0x00000362 0x0e414e46 f 0x00000001 0xf904b9f5 0x00000109 GWM MSG 0x0000b479 0x0000003b urn:tcomm-endpoint:device:deviceType:0:deviceSerialNumber:0 0x00000041 urn:tcomm-endpoint:service:serviceName:DeeWebsiteMessagingService {"command":"REGISTER_CONNECTION"}FABE'); this.messageId++; let msg = 'MSG 0x00000362 '; // Message-type and Channel = GW_CHANNEL; msg += this.encodeNumber(this.messageId) + ' f 0x00000001 '; const idx1 = msg.length; msg += '0x00000000 '; // Checksum! const idx2 = msg.length; msg += '0x00000109 '; // length content msg += 'GWM MSG 0x0000b479 0x0000003b urn:tcomm-endpoint:device:deviceType:0:deviceSerialNumber:0 0x00000041 urn:tcomm-endpoint:service:serviceName:DeeWebsiteMessagingService {"command":"REGISTER_CONNECTION"}FABE'; const completeBuffer = Buffer.from(msg, 'ascii'); const checksum = this.computeChecksum(completeBuffer, idx1, idx2); const checksumBuf = Buffer.from(this.encodeNumber(checksum)); checksumBuf.copy(completeBuffer, 39); return completeBuffer; } encodeGWRegisterAF() { this.messageId++; const completeBuffer = Buffer.alloc(0xe4); completeBuffer.write('MSG', 0,'ascii'); completeBuffer.writeUInt32BE(0x00000362, 3); // Message-type and Channel = GW_CHANNEL; completeBuffer.writeUInt32BE(this.messageId, 7); completeBuffer.write('f', 11, 'ascii'); completeBuffer.writeUInt32BE(0x00000001, 12); completeBuffer.writeUInt32BE(0x00000000, 16); // Checksum! completeBuffer.writeUInt32BE(0x000000e4, 20); // length content completeBuffer.write('GWM MSG 0x0000b479 0x0000003b urn:tcomm-endpoint:device:deviceType:0:deviceSerialNumber:0 0x00000041 urn:tcomm-endpoint:service:serviceName:DeeWebsiteMessagingService {"command":"REGISTER_CONNECTION"}FABE', 24, 'ascii'); const checksum = this.computeChecksum(completeBuffer, 16, 20); completeBuffer.writeUInt32BE(checksum, 16); // Checksum! return completeBuffer; } encodePing() { if (this.protocolName === 'A:H') { return this.encodePingAH(); } else { return this.encodePingAF(); } } encodePingAH() { function encode(a, b, c, h) { a = new Uint8Array(a, c, h); for (c = 0; c < h; c++) a[c] = b >> 8 * (h - 1 - c) & 255; } this.messageId++; let msg = 'MSG 0x00000065 '; // Message-type and Channel = CHANNEL_FOR_HEARTBEAT; msg += this.encodeNumber(this.messageId) + ' f 0x00000001 '; const idx1 = msg.length; msg += '0x00000000 '; // Checksum! const idx2 = msg.length; msg+= '0x00000062 '; // length content const completeBuffer = Buffer.alloc(0x62, 0); const startBuffer = Buffer.from(msg, 'ascii'); startBuffer.copy(completeBuffer); const header = 'PIN'; const payload = 'Regular'; // g = h.length const n = new ArrayBuffer(header.length + 4 + 8 + 4 + 2 * payload.length); let idx = 0; let u = new Uint8Array(n, idx, header.length); const l = 0; const e = Date.now(); for (let q = 0; q < header.length; q++) u[q] = header.charCodeAt(q); idx += header.length; encode(n, l, idx, 4); idx += 4; encode(n, e, idx, 8); idx += 8; encode(n, payload.length, idx, 4); idx += 4; u = new Uint8Array(n, idx, payload.length * 2); let q; for (q = 0; q < payload.length; q++) { u[q * 2] = 0; u[q * 2 + 1] = payload.charCodeAt(q); } const buf = Buffer.from(n); buf.copy(completeBuffer, msg.length); const buf2End = Buffer.from('FABE', 'ascii'); buf2End.copy(completeBuffer, msg.length + buf.length); const checksum = this.computeChecksum(completeBuffer, idx1, idx2); const checksumBuf = Buffer.from(this.encodeNumber(checksum)); checksumBuf.copy(completeBuffer, 39); return completeBuffer; } encodePingAF() { function encode(a, b, c, h) { a = new Uint8Array(a, c, h); for (c = 0; c < h; c++) a[c] = b >> 8 * (h - 1 - c) & 255; } this.messageId++; const completeBuffer = Buffer.alloc(0x3d); completeBuffer.write('MSG', 0,'ascii'); completeBuffer.writeUInt32BE(0x00000065, 3); // Message-type and Channel = CHANNEL_FOR_HEARTBEAT; completeBuffer.writeUInt32BE(this.messageId, 7); completeBuffer.write('f', 11, 'ascii'); completeBuffer.writeUInt32BE(0x00000001, 12); completeBuffer.writeUInt32BE(0x00000000, 16); // Checksum! completeBuffer.writeUInt32BE(0x0000003d, 20); // length content const header = 'PIN'; const payload = 'Regular'; // g = h.length const n = new ArrayBuffer(header.length + 4 + 8 + 4 + 2 * payload.length); let idx = 0; let u = new Uint8Array(n, idx, header.length); const l = 0; const e = Date.now(); for (let q = 0; q < header.length; q++) u[q] = header.charCodeAt(q); idx += header.length; encode(n, l, idx, 4); idx += 4; encode(n, e, idx, 8); idx += 8; encode(n, payload.length, idx, 4); idx += 4; u = new Uint8Array(n, idx, payload.length * 2); let q; for (q = 0; q < payload.length; q++) { u[q * 2] = 0; u[q * 2 + 1] = payload.charCodeAt(q); } const buf = Buffer.from(n); buf.copy(completeBuffer, 24); completeBuffer.write('FABE', buf.length + 24, 'ascii'); const checksum = this.computeChecksum(completeBuffer, 16, 20); completeBuffer.writeUInt32BE(checksum, 16); // Checksum! return completeBuffer; } computeChecksum(a, f, k) { function b(a, b) { for (a = c(a); 0 != b && 0 != a;) a = Math.floor(a / 2), b--; return a; } function c(a) { 0 > a && (a = 4294967295 + a + 1); return a; } if (k < f) throw 'Invalid checksum exclusion window!'; a = new Uint8Array(a); let h = 0, l = 0; for (let e = 0; e < a.length; e++) e != f ? (l += c(a[e] << ((e & 3 ^ 3) << 3)), h += b(l, 32), l = c(l & 4294967295)) : e = k - 1; for (; h;) l += h, h = b(l, 32), l &= 4294967295; return c(l); } parseIncomingMessage(data) { if (this.protocolName === 'A:H') { return this.parseIncomingMessageAH(data); } else { return this.parseIncomingMessageAF(data); } } parseIncomingMessageAH(data) { function readHex(index, length) { let str = data.toString('ascii', index, index + length); if (str.startsWith('0x')) str = str.substr(2); return parseInt(str, 16); } function readString(index, length) { return data.toString('ascii', index, index + length); } let idx = 0; const message = {}; message.service = readString(data.length - 4, 4); if (message.service === 'TUNE') { message.checksum = readHex(idx, 10); idx += 11; // 10 + delimiter; const contentLength = readHex(idx, 10); idx += 11; // 10 + delimiter; message.content = readString(idx, contentLength - 4 - idx); if (message.content.startsWith('{') && message.content.endsWith('}')) { try { message.content = JSON.parse(message.content); } catch (e) { // ignore } } } else if (message.service === 'FABE') { message.messageType = readString(idx, 3); idx += 4; message.channel = readHex(idx, 10); idx += 11; // 10 + delimiter; message.messageId = readHex(idx, 10); idx += 11; // 10 + delimiter; message.moreFlag = readString(idx, 1); idx += 2; // 1 + delimiter; message.seq = readHex(idx, 10); idx += 11; // 10 + delimiter; message.checksum = readHex(idx, 10); idx += 11; // 10 + delimiter; //const contentLength = readHex(idx, 10); idx += 11; // 10 + delimiter; message.content = {}; message.content.messageType = readString(idx, 3); idx += 4; if (message.channel === 0x361) { // GW_HANDSHAKE_CHANNEL if (message.content.messageType === 'ACK') { let length = readHex(idx, 10); idx += 11; // 10 + delimiter; message.content.protocolVersion = readString(idx, length); idx += length + 1; length = readHex(idx, 10); idx += 11; // 10 + delimiter; message.content.connectionUUID = readString(idx, length); idx += length + 1; message.content.established = readHex(idx, 10); idx += 11; // 10 + delimiter; message.content.timestampINI = readHex(idx, 18); idx += 19; // 18 + delimiter; message.content.timestampACK = readHex(idx, 18); idx += 19; // 18 + delimiter; } } else if (message.channel === 0x362) { // GW_CHANNEL if (message.content.messageType === 'GWM') { message.content.subMessageType = readString(idx, 3); idx += 4; message.content.channel = readHex(idx, 10); idx += 11; // 10 + delimiter; if (message.content.channel === 0xb479) { // DEE_WEBSITE_MESSAGING let length = readHex(idx, 10); idx += 11; // 10 + delimiter; message.content.destinationIdentityUrn = readString(idx, length); idx += length + 1; length = readHex(idx, 10); idx += 11; // 10 + delimiter; let idData = readString(idx, length); idx += length + 1; idData = idData.split(' '); message.content.deviceIdentityUrn = idData[0]; message.content.payload = idData[1]; if (!message.content.payload) { message.content.payload = readString(idx, data.length - 4 - idx); } if (message.content.payload.startsWith('{') && message.content.payload.endsWith('}')) { try { message.content.payload = JSON.parse(message.content.payload); if (message.content.payload && message.content.payload.payload && typeof message.content.payload.payload === 'string') { message.content.payload.payload = JSON.parse(message.content.payload.payload); } } catch (e) { // Ignore } } } } } else if (message.channel === 0x65) { // CHANNEL_FOR_HEARTBEAT idx -= 1; // no delimiter! message.content.payloadData = data.slice(idx, data.length - 4); } } //console.log(JSON.stringify(message, null, 4)); return message; } parseIncomingMessageAF(data) { function readHex(index, length) { let str = data.toString('ascii', index, index + length); if (str.startsWith('0x')) str = str.substr(2); return parseInt(str, 16); } function readString(index, length) { return data.toString('ascii', index, index + length); } let idx = 0; const message = {}; message.service = readString(data.length - 4, 4); //this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Incoming message Service: ' + message.service); if (message.service === 'TUNE') { message.checksum = readHex(idx, 10); idx += 11; // 10 + delimiter; const contentLength = readHex(idx, 10); idx += 11; // 10 + delimiter; message.content = readString(idx, contentLength - 4 - idx); if (message.content.startsWith('{') && message.content.endsWith('}')) { try { message.content = JSON.parse(message.content); } catch (e) { // Ignore } } } else if (message.service === 'FABE') { message.messageType = readString(0, 3); message.channel = data.readUInt32BE(3); message.messageId = data.readUInt32BE(7); message.moreFlag = data.readUInt32BE(11); message.seq = data.readUInt32BE(12); message.checksum = data.readUInt32BE(16); //const contentLength = data.readUInt32BE(20); message.content = {}; message.content.messageType = readString(24, 3); idx = 28; //this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Incoming message Service Channel: ' + message.channel); if (message.channel === 0x361) { // GW_HANDSHAKE_CHANNEL if (message.content.messageType === 'ACK') { let length = readHex(idx, 10); idx += 11; // 10 + delimiter; message.content.protocolVersion = readString(idx, length); idx += length + 1; length = readHex(idx, 10); idx += 11; // 10 + delimiter; message.content.connectionUUID = readString(idx, length); idx += length + 1; message.content.established = readHex(idx, 10); idx += 11; // 10 + delimiter; message.content.timestampINI = readHex(idx, 18); idx += 19; // 18 + delimiter; message.content.timestampACK = readHex(idx, 18); idx += 19; // 18 + delimiter; } } else if (message.channel === 0x362) { // GW_CHANNEL if (message.content.messageType === 'GWM') { message.content.subMessageType = readString(idx, 3); idx += 4; message.content.channel = readHex(idx, 10); idx += 11; // 10 + delimiter; if (message.content.channel === 0xb479) { // DEE_WEBSITE_MESSAGING let length = readHex(idx, 10); idx += 11; // 10 + delimiter; message.content.destinationIdentityUrn = readString(idx, length); idx += length + 1; length = readHex(idx, 10); idx += 11; // 10 + delimiter; let idData = readString(idx, length); idx += length + 1; idData = idData.split(' '); message.content.deviceIdentityUrn = idData[0]; message.content.payload = idData[1]; if (!message.content.payload) { message.content.payload = readString(idx, data.length - 4 - idx); } if (message.content.payload.startsWith('{') && message.content.payload.endsWith('}')) { try { message.content.payload = JSON.parse(message.content.payload); if (message.content.payload && message.content.payload.payload && typeof message.content.payload.payload === 'string') { message.content.payload.payload = JSON.parse(message.content.payload.payload); } } catch (e) { // Ignore } } } } } else if (message.channel === 0x65) { // CHANNEL_FOR_HEARTBEAT message.content.payloadData = data.slice(idx, data.length - 4); } } //console.log(JSON.stringify(message, null, 4)); //this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Parsed Message: ' + JSON.stringify(message)); return message; } disconnect() { if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = null; } if (this.pollingTimeout) { clearTimeout(this.pollingTimeout); this.pollingTimeout = null; } if (this.initTimeout) { clearTimeout(this.initTimeout); this.initTimeout = null; } this.stop = true; if (!this.websocket) return; try { this.websocket.close(); } catch (e) { this.connectionActive && this._options.logger && this._options.logger('Alexa-Remote WS-MQTT: Disconnect error: ' + e.message); } } } module.exports = AlexaWsMqtt;