/* jshint -W097 */
/* jshint -W030 */
/* jshint strict: false */
/* jslint node: true */
/* jslint esversion: 6 */
"use strict";
/**
* partly based on Amazon Alexa Remote Control (PLAIN shell)
* http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html AND on
* https://github.com/thorsten-gehrig/alexa-remote-control
* and much enhanced ...
*/
const https = require('https');
const querystring = require('querystring');
const url = require('url');
const os = require('os');
const cookieTools = require('cookie');
const amazonProxy = require('./proxy.js');
const amazonserver = process.argv[3];
const alexaserver = process.argv[4];
//const defaultAmazonPage = 'amazon.fr';
const defaultAmazonPage = amazonserver;
const defaultUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0';
const defaultUserAgentLinux = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36';
//const defaultUserAgentMacOs = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36';
const defaultAcceptLanguage = 'fr-FR';
let proxyServer;
let _options;
let Cookie = '';
function addCookies(Cookie, headers) {
if (!headers || !headers['set-cookie']) return Cookie;
const cookies = cookieTools.parse(Cookie);
for (let cookie of headers['set-cookie']) {
cookie = cookie.match(/^([^=]+)=([^;]+);.*/);
if (cookie && cookie.length === 3) {
if (cookie[1] === 'ap-fid' && cookie[2] === '""') continue;
if (cookies[cookie[1]] && cookies[cookie[1]] !== cookie[2]) {
_options.logger && _options.logger('Alexa-Cookie: Update Cookie ' + cookie[1] + ' = ' + cookie[2]);
}
else if (!cookies[cookie[1]]) {
_options.logger && _options.logger('Alexa-Cookie: Add Cookie ' + cookie[1] + ' = ' + cookie[2]);
}
cookies[cookie[1]] = cookie[2];
}
}
Cookie = '';
for (let name in cookies) {
if (!cookies.hasOwnProperty(name)) continue;
Cookie += name + '=' + cookies[name] + '; ';
}
Cookie = Cookie.replace(/[; ]*$/, '');
return Cookie;
}
function request(options, info, callback) {
_options.logger && _options.logger('Alexa-Cookie: Sending Request with ' + JSON.stringify(options));
if (typeof info === 'function') {
callback = info;
info = {
requests: []
};
}
let removeContentLength;
if (options.headers && options.headers['Content-Length']) {
if (!options.body) delete options.headers['Content-Length'];
} else if (options.body) {
if (!options.headers) options.headers = {};
options.headers['Content-Length'] = options.body.length;
removeContentLength = true;
}
let req = https.request(options, function (res) {
let body = "";
info.requests.push({options: options, response: res});
if (options.followRedirects !== false && res.statusCode >= 300 && res.statusCode < 400) {
_options.logger && _options.logger('Alexa-Cookie: Response (' + res.statusCode + ')' + (res.headers.location ? ' - Redirect to ' + res.headers.location : ''));
//options.url = res.headers.location;
let u = url.parse(res.headers.location);
if (u.host) options.host = u.host;
options.path = u.path;
options.method = 'GET';
options.body = '';
options.headers.Cookie = Cookie = addCookies(Cookie, res.headers);
res.socket.end();
return request(options, info, callback);
} else {
_options.logger && _options.logger('Alexa-Cookie: Response (' + res.statusCode + ')');
res.on('data', function (chunk) {
body += chunk;
});
res.on('end', function () {
if (removeContentLength) delete options.headers['Content-Length'];
res.socket.end();
callback && callback(0, res, body, info);
});
}
});
req.on('error', function (e) {
if (typeof callback === 'function' && callback.length >= 2) {
return callback(e, null, null, info);
}
});
if (options && options.body) {
req.write(options.body);
}
req.end();
}
function getFields(body) {
body = body.replace(/[\n\r]/g, ' ');
let re = /^.*?("hidden"\s*name=".*$)/;
let ar = re.exec(body);
if (!ar || ar.length < 2) return {};
let h;
re = /.*?name="([^"]+)"[\s^\s]*value="([^"]+).*?"/g;
let data = {};
while ((h = re.exec(ar[1])) !== null) {
if (h[1] !== 'rememberMe') {
data[h[1]] = h[2];
}
}
return data;
}
function initConfig() {
_options.amazonPage = _options.amazonPage || defaultAmazonPage;
if (_options.formerRegistrationData && _options.formerRegistrationData.amazonPage) _options.amazonPage = _options.formerRegistrationData.amazonPage;
_options.logger && _options.logger('Alexa-Cookie: Use as Login-Amazon-URL: ' + _options.amazonPage);
_options.logger && _options.logger('Alexa-Config (alexa-cookie.js): amazonserver=' + amazonserver);
_options.logger && _options.logger('Alexa-Config (alexa-cookie.js): alexaserver=' + alexaserver);
if (!_options.userAgent) {
let platform = os.platform();
if (platform === 'win32') {
_options.userAgent = defaultUserAgent;
}
/*else if (platform === 'darwin') {
_options.userAgent = defaultUserAgentMacOs;
}*/
else {
_options.userAgent = defaultUserAgentLinux;
}
}
_options.logger && _options.logger('Alexa-Cookie: Use as User-Agent: ' + _options.userAgent);
_options.acceptLanguage = _options.acceptLanguage || defaultAcceptLanguage;
_options.logger && _options.logger('Alexa-Cookie: Use as Accept-Language: ' + _options.acceptLanguage);
if (_options.setupProxy && !_options.proxyOwnIp) {
_options.logger && _options.logger('Alexa-Cookie: Own-IP Setting muissing for Proxy. Disabling!');
_options.setupProxy = false;
}
if (_options.setupProxy) {
_options.setupProxy = true;
_options.proxyPort = _options.proxyPort || 0;
_options.proxyListenBind = _options.proxyListenBind || '0.0.0.0';
_options.logger && _options.logger('Alexa-Cookie: Proxy-Mode enabled if needed: ' + _options.proxyOwnIp + ':' + _options.proxyPort + ' to listen on ' + _options.proxyListenBind);
} else {
_options.setupProxy = false;
_options.logger && _options.logger('Alexa-Cookie: Proxy mode disabled');
}
_options.proxyLogLevel = _options.proxyLogLevel || 'warn';
_options.amazonPageProxyLanguage = _options.amazonPageProxyLanguage || 'fr_FR';
if (_options.formerRegistrationData) _options.proxyOnly = true;
}
function getCSRFFromCookies(cookie, _options, callback) {
// get CSRF
let options = {
'host': 'alexa.' + _options.amazonPage,
//'path': '/api/language',
'path': '/templates/oobe/d-device-pick.handlebars',
'method': 'GET',
'headers': {
'DNT': '1',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
'Connection': 'keep-alive',
'Referer': 'https://alexa.' + _options.amazonPage + '/spa/index.html',
'Cookie': cookie,
'Accept': '*/*',
'Origin': 'https://alexa.' + _options.amazonPage
}
};
_options.logger && _options.logger('Alexa-Cookie: Step 4: get CSRF');
request(options, (error, response) => {
cookie = addCookies(cookie, response.headers);
let ar = /csrf=([^;]+)/.exec(cookie);
let csrf = ar ? ar[1] : undefined;
_options.logger && _options.logger('Alexa-Cookie: Result: csrf=' + csrf + ', Cookie=' + cookie);
callback && callback(null, {
cookie: cookie,
csrf: csrf
});
});
}
function generateAlexaCookie(email, password, __options, callback) {
if (email !== undefined && typeof email !== 'string') {
callback = __options;
__options = password;
password = email;
email = null;
}
if (password !== undefined && typeof password !== 'string') {
callback = __options;
__options = password;
password = null;
}
if (typeof __options === 'function') {
callback = __options;
__options = {};
}
_options = __options;
if (!email || !password) {
__options.proxyOnly = true;
}
initConfig();
if (!_options.proxyOnly) {
// get first cookie and write redirection target into referer
let options = {
host: 'alexa.' + _options.amazonPage,
path: '',
method: 'GET',
headers: {
'DNT': '1',
'Upgrade-Insecure-Requests': '1',
'User-Agent': _options.userAgent,
'Accept-Language': _options.acceptLanguage,
'Connection': 'keep-alive',
'Accept': '*/*'
},
};
_options.logger && _options.logger('Alexa-Cookie: Step 1: get first cookie and authentication redirect');
request(options, (error, response, body, info) => {
if (error) {
callback && callback(error, null);
return;
}
let lastRequestOptions = info.requests[info.requests.length - 1].options;
// login empty to generate session
Cookie = addCookies(Cookie, response.headers);
let options = {
host: 'www.' + _options.amazonPage,
path: '/ap/signin',
method: 'POST',
headers: {
'DNT': '1',
'Upgrade-Insecure-Requests': '1',
'User-Agent': _options.userAgent,
'Accept-Language': _options.acceptLanguage,
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': 'https://' + lastRequestOptions.host + lastRequestOptions.path,
'Cookie': Cookie,
'Accept': '*/*'
},
gzip: true,
body: querystring.stringify(getFields(body))
};
_options.logger && _options.logger('Alexa-Cookie: Step 2: login empty to generate session');
request(options, (error, response, body) => {
if (error) {
callback && callback(error, null);
return;
}
// login with filled out form
// !!! referer now contains session in URL
options.host = 'www.' + _options.amazonPage;
options.path = '/ap/signin';
options.method = 'POST';
options.headers.Cookie = Cookie = addCookies(Cookie, response.headers);
let ar = options.headers.Cookie.match(/session-id=([^;]+)/);
options.headers.Referer = `https://www.${_options.amazonPage}/ap/signin/${ar[1]}`;
options.body = getFields(body);
options.body.email = email || '';
options.body.password = password || '';
options.body = querystring.stringify(options.body, null, null, {encodeURIComponent: encodeURIComponent});
_options.logger && _options.logger('Alexa-Cookie: Step 3: login with filled form, referer contains session id');
request(options, (error, response, body, info) => {
if (error) {
callback && callback(error, null);
return;
}
let lastRequestOptions = info.requests[info.requests.length - 1].options;
// check whether the login has been successful or exit otherwise
if (!lastRequestOptions.host.startsWith('alexa') || !lastRequestOptions.path.endsWith('.html')) {
let errMessage = 'Login unsuccessfull. Please check credentials.';
const amazonMessage = body.match(/auth-warning-message-box[\S\s]*"a-alert-heading">([^<]*)[\S\s]*
<[^>]*>\s*([^<\n]*)\s*);
if (amazonMessage && amazonMessage[1] && amazonMessage[2]) {
errMessage = `Amazon-Login-Error: ${amazonMessage[1]}: ${amazonMessage[2]}`;
}
if (_options.setupProxy) {
if (proxyServer) {
errMessage += ` You can try to get the cookie manually by opening http://${_options.proxyOwnIp}:${_options.proxyPort}/ with your browser.`;
} else {
amazonProxy.initAmazonProxy(_options, prepareResult,
(server) => {
proxyServer = server;
if (_options.proxyPort === 0) {
_options.proxyPort = proxyServer.address().port;
}
errMessage += ` You can try to get the cookie manually by opening http://${_options.proxyOwnIp}:${_options.proxyPort}/ with your browser.`;
callback && callback(new Error(errMessage), null);
}
);
return;
}
}
callback && callback(new Error(errMessage), null);
return;
}
return getCSRFFromCookies(Cookie, _options, callback);
});
});
});
}
else {
amazonProxy.initAmazonProxy(_options, prepareResult, (server) => {
proxyServer = server;
if (_options.proxyPort === 0) {
_options.proxyPort = proxyServer.address().port;
}
const errMessage = `You can try to get the cookie manually by opening http://${_options.proxyOwnIp}:${_options.proxyPort}/ with your browser.`;
callback && callback(new Error(errMessage), null);
});
}
function prepareResult(err, data) {
if (err || !data.accessToken) {
callback && callback(err, data.loginCookie);
return;
}
handleTokenRegistration(_options, data, callback);
}
}
function handleTokenRegistration(_options, loginData, callback) {
_options.logger && _options.logger('Handle token registration Start: ' + JSON.stringify(loginData));
let deviceSerial;
if (!_options.formerRegistrationData || !_options.formerRegistrationData.deviceSerial) {
const deviceSerialBuffer = Buffer.alloc(16);
for (let i = 0; i < 16; i++) {
deviceSerialBuffer.writeUInt8(Math.floor(Math.random() * 255), i);
}
deviceSerial = deviceSerialBuffer.toString('hex');
}
else {
_options.logger && _options.logger('Proxy Init: reuse deviceSerial from former data');
deviceSerial = _options.formerRegistrationData.deviceSerial;
}
loginData.deviceSerial = deviceSerial;
const cookies = cookieTools.parse(loginData.loginCookie);
Cookie = loginData.loginCookie;
/*
Register App
*/
const registerData = {
"requested_extensions": [
"device_info",
"customer_info"
],
"cookies": {
"website_cookies": [
/*{
"Value": cookies["session-id-time"],
"Name": "session-id-time"
}*/
],
"domain": ".amazon.com"
},
"registration_data": {
"domain": "Device",
"app_version": "2.2.223830.0",
"device_type": "A2IVLV5VM2W81",
"device_name": "%FIRST_NAME%\u0027s%DUPE_STRATEGY_1ST%ioBroker Alexa2",
"os_version": "11.4.1",
"device_serial": deviceSerial,
"device_model": "iPhone",
"app_name": "ioBroker Alexa2",
"software_version": "1"
},
"auth_data": {
"access_token": loginData.accessToken
},
"user_context_map": {
"frc": cookies.frc
},
"requested_token_type": [
"bearer",
"mac_dms",
"website_cookies"
]
};
for (let key in cookies) {
if (!cookies.hasOwnProperty(key)) continue;
registerData.cookies.website_cookies.push({
"Value": cookies[key],
"Name": key
});
}
let options = {
host: 'api.amazon.com',
path: '/auth/register',
method: 'POST',
headers: {
'User-Agent': 'AmazonWebView/Amazon Alexa/2.2.223830.0/iOS/11.4.1/iPhone',
'Accept-Language': _options.acceptLanguage,
'Accept-Charset': 'utf-8',
'Connection': 'keep-alive',
'Content-Type': 'application/json',
'Cookie': loginData.loginCookie,
'Accept': '*/*',
'x-amzn-identity-auth-domain': 'api.amazon.com'
},
body: JSON.stringify(registerData)
};
_options.logger && _options.logger('Alexa-Cookie: Register App');
_options.logger && _options.logger(JSON.stringify(options));
request(options, (error, response, body) => {
if (error) {
callback && callback(error, null);
return;
}
try {
if (typeof body !== 'object') body = JSON.parse(body);
}
catch (err) {
_options.logger && _options.logger('Register App Response: ' + JSON.stringify(body));
callback && callback(err, null);
return;
}
_options.logger && _options.logger('Register App Response: ' + JSON.stringify(body));
if (!body.response || !body.response.success || !body.response.success.tokens || !body.response.success.tokens.bearer) {
callback && callback(new Error('No tokens in Register response'), null);
return;
}
Cookie = addCookies(Cookie, response.headers);
loginData.refreshToken = body.response.success.tokens.bearer.refresh_token;
loginData.tokenDate = Date.now();
/*
Get Amazon Marketplace Country
*/
let options = {
host: 'alexa.amazon.com',
path: '/api/users/me?platform=ios&version=2.2.223830.0',
method: 'GET',
headers: {
'User-Agent': 'AmazonWebView/Amazon Alexa/2.2.223830.0/iOS/11.4.1/iPhone',
'Accept-Language': _options.acceptLanguage,
'Accept-Charset': 'utf-8',
'Connection': 'keep-alive',
'Accept': 'application/json',
'Cookie': Cookie
}
};
_options.logger && _options.logger('Alexa-Cookie: Get User data');
_options.logger && _options.logger(JSON.stringify(options));
request(options, (error, response, body) => {
if (!error) {
try {
if (typeof body !== 'object') body = JSON.parse(body);
} catch (err) {
_options.logger && _options.logger('Get User data Response: ' + JSON.stringify(body));
callback && callback(err, null);
return;
}
_options.logger && _options.logger('Get User data Response: ' + JSON.stringify(body));
Cookie = addCookies(Cookie, response.headers);
if (body.marketPlaceDomainName) {
const pos = body.marketPlaceDomainName.indexOf('.');
if (pos !== -1) _options.amazonPage = body.marketPlaceDomainName.substr(pos+1);
}
loginData.amazonPage = _options.amazonPage;
}
else if (error && !_options.amazonPage) {
callback && callback(error, null);
return;
}
else if (error && !_options.formerRegistrationData.amazonPage && _options.amazonPage) {
_options.logger && _options.logger('Continue with externally set amazonPage: ' + _options.amazonPage);
}
else if (error) {
_options.logger && _options.logger('Ignore error while getting user data and amazonPage because previously set amazonPage is available');
}
loginData.loginCookie = Cookie;
getLocalCookies(loginData.amazonPage, loginData.refreshToken, (err, localCookie) => {
if (err) {
callback && callback(err, null);
}
loginData.localCookie = localCookie;
getCSRFFromCookies(loginData.localCookie, _options, (err, resData) => {
if (err) {
callback && callback(new Error('Error getting csrf for ' + loginData.amazonPage), null);
return;
}
loginData.localCookie = resData.cookie;
loginData.csrf = resData.csrf;
delete loginData.accessToken;
_options.logger && _options.logger('Final Registraton Result: ' + JSON.stringify(loginData));
callback && callback(null, loginData);
});
});
});
});
}
function getLocalCookies(amazonPage, refreshToken, callback) {
Cookie = ''; // Reset because we are switching domains
/*
Token Exchange to Amazon Country Page
*/
const exchangeParams = {
'di.os.name': 'iOS',
'app_version': '2.2.223830.0',
'domain': '.' + amazonPage,
'source_token': refreshToken,
'requested_token_type': 'auth_cookies',
'source_token_type': 'refresh_token',
'di.hw.version': 'iPhone',
'di.sdk.version': '6.10.0',
'cookies': Buffer.from('{„cookies“:{".' + amazonPage + '":[]}}').toString('base64'),
'app_name': 'Amazon Alexa',
'di.os.version': '11.4.1'
};
let options = {
host: 'www.' + amazonPage,
path: '/ap/exchangetoken',
method: 'POST',
headers: {
'User-Agent': 'AmazonWebView/Amazon Alexa/2.2.223830.0/iOS/11.4.1/iPhone',
'Accept-Language': _options.acceptLanguage,
'Accept-Charset': 'utf-8',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': '*/*'
},
body: querystring.stringify(exchangeParams, null, null, {
encodeURIComponent: encodeURIComponent
})
};
_options.logger && _options.logger('Alexa-Cookie: Exchange tokens for ' + amazonPage);
_options.logger && _options.logger(JSON.stringify(options));
request(options, (error, response, body) => {
if (error) {
callback && callback(error, null);
return;
}
try {
if (typeof body !== 'object') body = JSON.parse(body);
} catch (err) {
_options.logger && _options.logger('Exchange Token Response: ' + JSON.stringify(body));
callback && callback(err, null);
return;
}
_options.logger && _options.logger('Exchange Token Response: ' + JSON.stringify(body));
if (!body.response || !body.response.tokens || !body.response.tokens.cookies) {
callback && callback(new Error('No cookies in Exchange response'), null);
return;
}
if (!body.response.tokens.cookies['.' + amazonPage]) {
callback && callback(new Error('No cookies for ' + amazonPage + ' in Exchange response'), null);
return;
}
Cookie = addCookies(Cookie, response.headers);
const cookies = cookieTools.parse(Cookie);
body.response.tokens.cookies['.' + amazonPage].forEach((cookie) => {
if (cookies[cookie.Name] && cookies[cookie.Name] !== cookie.Value) {
_options.logger && _options.logger('Alexa-Cookie: Update Cookie ' + cookie.Name + ' = ' + cookie.Value);
}
else if (!cookies[cookie.Name]) {
_options.logger && _options.logger('Alexa-Cookie: Add Cookie ' + cookie.Name + ' = ' + cookie.Value);
}
cookies[cookie.Name] = cookie.Value;
});
let localCookie = '';
for (let name in cookies) {
if (!cookies.hasOwnProperty(name)) continue;
localCookie += name + '=' + cookies[name] + '; ';
}
localCookie = localCookie.replace(/[; ]*$/, '');
callback && callback(null, localCookie);
});
}
function refreshAlexaCookie(__options, callback) {
if (!__options || !__options.formerRegistrationData || !__options.formerRegistrationData.loginCookie || !__options.formerRegistrationData.refreshToken) {
callback && callback(new Error('No former registration data provided for Cookie Refresh'), null);
return;
}
if (typeof __options === 'function') {
callback = __options;
__options = {};
}
_options = __options;
__options.proxyOnly = true;
initConfig();
const refreshData = {
"app_name": "ioBroker Alexa2",
"app_version": "2.2.223830.0",
"di.sdk.version": "6.10.0",
"source_token": _options.formerRegistrationData.refreshToken,
"package_name": "com.amazon.echo",
"di.hw.version": "iPhone",
"platform": "iOS",
"requested_token_type": "access_token",
"source_token_type": "refresh_token",
"di.os.name": "iOS",
"di.os.version": "11.4.1",
"current_version": "6.10.0"
};
let options = {
host: 'api.amazon.com',
path: '/auth/token',
method: 'POST',
headers: {
'User-Agent': 'AmazonWebView/Amazon Alexa/2.2.223830.0/iOS/11.4.1/iPhone',
'Accept-Language': _options.acceptLanguage,
'Accept-Charset': 'utf-8',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': _options.formerRegistrationData.loginCookie,
'Accept': 'application/json',
'x-amzn-identity-auth-domain': 'api.amazon.com'
},
body: querystring.stringify(refreshData)
};
Cookie = _options.formerRegistrationData.loginCookie;
_options.logger && _options.logger('Alexa-Cookie: Refresh Token');
_options.logger && _options.logger(JSON.stringify(options));
request(options, (error, response, body) => {
if (error) {
callback && callback(error, null);
return;
}
try {
if (typeof body !== 'object') body = JSON.parse(body);
} catch (err) {
_options.logger && _options.logger('Refresh Token Response: ' + JSON.stringify(body));
callback && callback(err, null);
return;
}
_options.logger && _options.logger('Refresh Token Response: ' + JSON.stringify(body));
_options.formerRegistrationData.loginCookie = addCookies(_options.formerRegistrationData.loginCookie, response.headers);
if (!body.access_token) {
callback && callback(new Error('No new access token in Refresh Token response'), null);
return;
}
_options.formerRegistrationData.loginCookie = addCookies(Cookie, response.headers);
_options.formerRegistrationData.accessToken = body.access_token;
getLocalCookies('amazon.com', _options.formerRegistrationData.refreshToken, (err, comCookie) => {
if (err) {
callback && callback(err, null);
}
// Restore frc and map-md
const initCookies = cookieTools.parse(_options.formerRegistrationData.loginCookie);
let newCookie = 'frc=' + initCookies.frc + '; ';
newCookie += 'map-md=' + initCookies['map-md'] + '; ';
newCookie += comCookie;
_options.formerRegistrationData.loginCookie = newCookie;
handleTokenRegistration(_options, _options.formerRegistrationData, callback);
});
});
}
function stopProxyServer(callback) {
if (proxyServer) {
proxyServer.close(() => {
callback && callback();
});
}
proxyServer = null;
}
module.exports.generateAlexaCookie = generateAlexaCookie;
module.exports.refreshAlexaCookie = refreshAlexaCookie;
module.exports.stopProxyServer = stopProxyServer;