alexa-remote2
Version:
Remote Control for amazon echo devices
300 lines (272 loc) • 13.8 kB
JavaScript
const http2 = require('http2');
const EventEmitter = require('events');
class AlexaHttp2Push extends EventEmitter {
constructor(options, update_access_token) {
super();
this._options = options;
this.stop = false;
this.client = null;
this.stream = null;
this.pingPongInterval = null;
this.errorRetryCounter = 0;
this.reconnectTimeout = null;
this.pongTimeout = null;
this.initTimeout = null;
this.connectionActive = false;
this.access_token = null;
this.update_access_token = update_access_token;
this.inClosing = false;
}
isConnected() {
return this.connectionActive;
}
connect() {
this.inClosing = false;
this.update_access_token(token => {
this.access_token = token;
let host = 'bob-dispatch-prod-eu.amazon.com';
if (this._options.pushDispatchHost) {
host = this._options.pushDispatchHost;
} else if (this._options.amazonPage === 'amazon.com') {
host = 'bob-dispatch-prod-na.amazon.com';
} else if (this._options.amazonPage === 'amazon.ca') {
host = 'bob-dispatch-prod-na.amazon.com';
} else if (this._options.amazonPage === 'amazon.com.mx') {
host = 'bob-dispatch-prod-na.amazon.com';
} else if (this._options.amazonPage === 'amazon.com.br') {
host = 'bob-dispatch-prod-na.amazon.com';
} else if (this._options.amazonPage === 'amazon.co.jp') {
host = 'bob-dispatch-prod-fe.amazon.com';
} else if (this._options.amazonPage === 'amazon.com.au') {
host = 'bob-dispatch-prod-fe.amazon.com';
} else if (this._options.amazonPage === 'amazon.com.in') {
host = 'bob-dispatch-prod-fe.amazon.com';
} else if (this._options.amazonPage === 'amazon.co.nz') {
host = 'bob-dispatch-prod-fe.amazon.com';
}
this._options.logger && this._options.logger(`Alexa-Remote HTTP2-PUSH: Use host ${host}`);
const http2Options = {
':method': 'GET',
':path': '/v20160207/directives',
':authority': host,
':scheme': 'https',
'authorization': `Bearer ${this.access_token}`,
'accept-encoding': 'gzip',
'user-agent': 'okhttp/4.3.2-SNAPSHOT',
};
const onHttp2Close = (code, reason, immediateReconnect) => {
if (this.inClosing) return;
this.inClosing = true;
if (reason) {
reason = reason.toString();
}
try {
this.stream && this.stream.destroy();
} catch (err) {
// ignore
}
try {
this.client && this.client.destroy();
} catch (err) {
// ignore
}
this.client = null;
this.stream = null;
this.connectionActive = false;
this._options.logger && this._options.logger(`Alexa-Remote HTTP2-PUSH: 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 (this.stop) {
return;
}
if (this.errorRetryCounter > 100) {
this.emit('disconnect', false, 'Too many failed retries. Check cookie and data');
return;
}
this.errorRetryCounter++;
const retryDelay = (immediateReconnect || this.errorRetryCounter === 1) ? 1 : Math.min(60, this.errorRetryCounter * 5);
this._options.logger && this._options.logger(`Alexa-Remote HTTP2-PUSH: Retry Connection in ${retryDelay}s`);
if (code !== undefined || reason !== undefined) {
this.emit('disconnect', true, `Retry Connection in ${retryDelay}s (${code}: ${reason})`);
} else {
this.emit('disconnect', true, `Retry Connection in ${retryDelay}s`);
}
this.reconnectTimeout && clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = setTimeout(() => {
this.reconnectTimeout = null;
this.connect();
}, retryDelay * 1000);
};
const onPingResponse = (resetErrorCount) => {
if (this.initTimeout) {
clearTimeout(this.initTimeout);
this.initTimeout = null;
this._options.logger && this._options.logger('Alexa-Remote HTTP2-PUSH: Initialization completed');
this.emit('connect');
}
if (this.pongTimeout) {
clearTimeout(this.pongTimeout);
this.pongTimeout = null;
}
this.connectionActive = true;
if (resetErrorCount) {
this.errorRetryCounter = 0;
}
};
try {
this.client = http2.connect(`https://${http2Options[':authority']}`, () => {
if (!this.client) {
return;
}
try {
this.stream = this.client.request(http2Options);
} catch (error) {
this._options.logger && this._options.logger(`Alexa-Remote HTTP2-PUSH: Error on Request ${error.message}`);
this.emit('error', error);
return;
}
this.stream.on('response', async (headers) => {
if (headers[':status'] === 403) {
this._options.logger && this._options.logger('Alexa-Remote HTTP2-PUSH: Error 403 .... refresh token');
this.update_access_token(token => {
if (token) {
this.access_token = token;
}
onHttp2Close(headers[':status'], undefined, this.errorRetryCounter < 3);
});
} else if (headers[':status'] !== 200) {
onHttp2Close(headers[':status']);
}
});
this.stream.on('data', (chunk) => {
if (this.stop) {
this.stream && this.stream.end();
this.client && this.client.close();
return;
}
chunk = chunk.toString();
if (chunk.startsWith('------')) {
this.client.ping(() => onPingResponse(false));
this.pingPongInterval = setInterval(() => {
if (!this.stream || !this.client) {
return;
}
this._options.logger && this._options.logger('Alexa-Remote HTTP2-PUSH: Send Ping');
// console.log('SEND: ' + msg.toString('hex'));
try {
this.client.ping(() => onPingResponse(true));
} catch (error) {
this._options.logger && this._options.logger(`Alexa-Remote HTTP2-PUSH: Error on Ping ${error.message}`);
}
this.pongTimeout = setTimeout(() => {
this.pongTimeout = null;
this._options.logger && this._options.logger('Alexa-Remote HTTP2-PUSH: No Pong received after 30s');
this.stream && this.stream.end();
this.client && this.client.close();
this.connectionActive = false;
}, 30000);
}, 180000);
return;
}
if (chunk.startsWith('Content-Type: application/json')) {
const json_start = chunk.indexOf('{');
const json_end = chunk.lastIndexOf('}');
if (json_start === -1 || json_end === -1) {
this._options.logger && this._options.logger(`Alexa-Remote HTTP2-PUSH: Unexpected ResponseCould not find json in chunk: ${chunk}`);
return;
}
const message = chunk.substring(json_start, json_end + 1);
try {
const data = JSON.parse(message);
if (!data || !data.directive || !data.directive.payload || !Array.isArray(data.directive.payload.renderingUpdates)) {
this._options.logger && this._options.logger(`Alexa-Remote HTTP2-PUSH: Unexpected ResponseCould not find renderingUpdates in json: ${message}`);
return;
}
data.directive.payload.renderingUpdates.forEach(update => {
if (!update || !update.resourceMetadata) {
this._options.logger && this._options.logger(`Alexa-Remote HTTP2-PUSH: Unexpected ResponseCould not find resourceMetadata in renderingUpdates: ${message}`);
}
const dataContent = JSON.parse(update.resourceMetadata);
const command = dataContent.command;
const payload = JSON.parse(dataContent.payload);
this._options.logger && this._options.logger(`Alexa-Remote HTTP2-PUSH: Command ${command}: ${JSON.stringify(payload, null, 4)}`);
this.emit('command', command, payload);
});
} catch (err) {
this.emit('unexpected-response', `Could not parse json: ${message}: ${err.message}`);
}
}
});
this.stream.on('close', onHttp2Close);
this.stream.on('error', (error) => {
this._options.logger && this._options.logger(`Alexa-Remote HTTP2-PUSH: Stream-Error: ${error}`);
this.emit('error', error);
this.stream && this.stream.end();
this.client && this.client.close();
});
});
this.client.on('close', onHttp2Close);
this.client.on('error', (error) => {
this._options.logger && this._options.logger(`Alexa-Remote HTTP2-PUSH: Client-Error: ${error}`);
this.emit('error', error);
this.stream && this.stream.end();
this.client && this.client.close();
});
}
catch (err) {
this._options.logger && this._options.logger(`Alexa-Remote HTTP2-PUSH: Error on Init ${err.message}`);
this._options.logger && this._options.logger(err.stack);
this.emit('error', err);
return;
}
this.initTimeout && clearTimeout(this.initTimeout);
this.initTimeout = setTimeout(() => {
this._options.logger && this._options.logger('Alexa-Remote HTTP2-PUSH: Initialization not done within 30s');
try {
this.stream && this.stream.end();
this.client && this.client.close();
} catch (err) {
// just make sure
}
if (this.stream || !this.reconnectTimeout) { // it seems no close was emitted so far?!
onHttp2Close();
}
}, 30000);
});
}
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.client && !this.stream) {
return;
}
try {
this.stream && this.stream.end();
this.client && this.client.close();
} catch (e) {
this.connectionActive && this._options.logger && this._options.logger(`Alexa-Remote HTTP2-PUSH: Disconnect error: ${e.message}`);
}
}
}
module.exports = AlexaHttp2Push;