alexa-cookie2
Version:
Generate Cookie and CSRF for Alexa Remote
826 lines (741 loc) • 42.2 kB
JavaScript
/**
* 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('./lib/proxy.js');
const defaultAmazonPage = 'amazon.de';
const defaultUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36';
const defaultUserAgentLinux = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 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 defaultProxyCloseWindowHTML = '<b>Amazon Alexa Cookie successfully retrieved. You can close the browser.</b>';
const defaultAcceptLanguage = 'de-DE';
const apiCallVersion = '2.2.651540.0';
const apiCallUserAgent = 'AmazonWebView/Amazon Alexa/2.2.651540.0/iOS/18.3.1/iPhone';
const defaultAppName = 'ioBroker Alexa2';
const csrfOptions = [
'/api/language',
'/spa/index.html',
'/api/devices-v2/device?cached=false',
'/templates/oobe/d-device-pick.handlebars',
'/api/strings'
];
function AlexaCookie() {
if (!(this instanceof AlexaCookie)) return new AlexaCookie();
let proxyServer;
let _options;
let Cookie = '';
const 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 (const name of Object.keys(cookies)) {
Cookie += `${name}=${cookies[name]}; `;
}
Cookie = Cookie.replace(/[; ]*$/, '');
return Cookie;
};
const 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;
}
const req = https.request(options, (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;
const 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 && res.socket.end();
return request(options, info, callback);
} else {
_options.logger && _options.logger(`Alexa-Cookie: Response (${res.statusCode})`);
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', () => {
if (removeContentLength) delete options.headers['Content-Length'];
res.socket && res.socket.end();
callback && callback(0, res, body, info);
});
}
});
req.on('error', (e) => {
if (typeof callback === 'function' && callback.length >= 2) {
return callback(e, null, null, info);
}
});
if (options && options.body) {
req.write(options.body);
}
req.end();
};
const getFields = body => {
body = body.replace(/[\n\r]/g, ' ');
let re = /^.*?("hidden"\s*name=".*$)/;
const ar = re.exec(body);
if (!ar || ar.length < 2) return {};
let h;
re = /.*?name="([^"]+)"[\s^\s]*value="([^"]+).*?"/g;
const data = {};
while ((h = re.exec(ar[1])) !== null) {
if (h[1] !== 'rememberMe') {
data[h[1]] = h[2];
}
}
return data;
};
const 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.baseAmazonPage = _options.baseAmazonPage || 'amazon.com';
_options.logger && _options.logger(`Alexa-Cookie: Use as Base-Amazon-URL: ${_options.baseAmazonPage}`);
_options.deviceAppName = _options.deviceAppName || defaultAppName;
_options.logger && _options.logger(`Alexa-Cookie: Use as Device-App-Name: ${_options.deviceAppName}`);
if (!_options.baseAmazonPageHandle && _options.baseAmazonPageHandle !== '') {
const amazonDomain = _options.baseAmazonPage.substr(_options.baseAmazonPage.lastIndexOf('.') + 1);
if (amazonDomain === 'jp') {
_options.baseAmazonPageHandle = `_${amazonDomain}`;
}
else if (amazonDomain !== 'com') {
//_options.baseAmazonPageHandle = '_' + amazonDomain;
_options.baseAmazonPageHandle = '';
}
else {
_options.baseAmazonPageHandle = '';
}
}
if (!_options.userAgent) {
const 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}`);
_options.proxyCloseWindowHTML = _options.proxyCloseWindowHTML || defaultProxyCloseWindowHTML;
if (_options.setupProxy && !_options.proxyOwnIp) {
_options.logger && _options.logger('Alexa-Cookie: Own-IP Setting missing 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 || 'de_DE';
if (_options.formerRegistrationData) _options.proxyOnly = true;
};
const getCSRFFromCookies = (cookie, _options, callback) => {
// get CSRF
const csrfUrls = csrfOptions;
function csrfTry() {
const path = csrfUrls.shift();
const options = {
'host': `alexa.${_options.amazonPage}`,
'path': path,
'method': 'GET',
'headers': {
'DNT': '1',
'User-Agent': _options.userAgent,
'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 via ${path}`);
request(options, (error, response) => {
cookie = addCookies(cookie, response ? response.headers : null);
const ar = /csrf=([^;]+)/.exec(cookie);
const csrf = ar ? ar[1] : undefined;
_options.logger && _options.logger(`Alexa-Cookie: Result: csrf=${csrf}, Cookie=${cookie}`);
if (!csrf && csrfUrls.length) {
csrfTry();
return;
}
callback && callback(null, {
cookie: cookie,
csrf: csrf
});
});
}
csrfTry();
};
this.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
const 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;
}
const lastRequestOptions = info.requests[info.requests.length - 1].options;
// login empty to generate session
Cookie = addCookies(Cookie, response.headers);
const 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);
const 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;
}
const 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]*<li><[^>]*>\s*([^<\n]*)\s*</);
if (amazonMessage && amazonMessage[1] && amazonMessage[2]) {
errMessage = `Amazon-Login-Error: ${amazonMessage[1]}: ${amazonMessage[2]}`;
}
if (_options.setupProxy) {
if (proxyServer) {
errMessage += ` Please open http://${_options.proxyOwnIp}:${_options.proxyPort}/ with your browser and login to Amazon. The cookie will be output here after successfull login.`;
} else {
amazonProxy.initAmazonProxy(_options, prepareResult,
(server) => {
if (!server) {
return callback && callback(new Error('Proxy could not be initialized'), null);
}
proxyServer = server;
if (!_options.proxyPort || _options.proxyPort === 0) {
_options.proxyPort = proxyServer.address().port;
}
errMessage += ` Please open http://${_options.proxyOwnIp}:${_options.proxyPort}/ with your browser and login to Amazon. The cookie will be output here after successfull login.`;
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) => {
if (!server) {
callback && callback(new Error('Proxy Server could not be initialized. Check Logs.'), null);
return;
}
proxyServer = server;
if (!_options.proxyPort || _options.proxyPort === 0) {
_options.proxyPort = proxyServer.address().port;
}
const errMessage = `Please open http://${_options.proxyOwnIp}:${_options.proxyPort}/ with your browser and login to Amazon. The cookie will be output here after successfull login.`;
callback && callback(new Error(errMessage), null);
});
}
function prepareResult(err, data) {
if (err || !data.authorization_code) {
callback && callback(err, data.loginCookie);
return;
}
handleTokenRegistration(_options, data, callback);
}
};
this.getDeviceAppName = () => {
return (_options && _options.deviceAppName) || defaultAppName;
};
const handleTokenRegistration = (_options, loginData, callback) => {
_options.logger && _options.logger(`Handle token registration Start: ${JSON.stringify(loginData)}`);
loginData.deviceAppName = _options.deviceAppName;
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': [],
'domain': `.${_options.baseAmazonPage}`
},
'registration_data': {
'domain': 'Device',
'app_version': apiCallVersion,
'device_type': 'A2IVLV5VM2W81',
'device_name': '%FIRST_NAME%\u0027s%DUPE_STRATEGY_1ST%' + _options.deviceAppName,
'os_version': '18.3.1',
'device_serial': deviceSerial,
'device_model': 'iPhone',
'app_name': _options.deviceAppName,
'software_version': '1'
},
'auth_data': {
// Filled below
},
'user_context_map': {
'frc': cookies.frc
},
'requested_token_type': [
'bearer',
'mac_dms',
'website_cookies'
]
};
if (loginData.accessToken) {
registerData.auth_data = {
'access_token': loginData.accessToken
};
} else if (loginData.authorization_code && loginData.verifier) {
registerData.auth_data = {
'client_id' : loginData.deviceId,
'authorization_code' : loginData.authorization_code,
'code_verifier' : loginData.verifier,
'code_algorithm' : 'SHA-256',
'client_domain' : 'DeviceLegacy'
};
}
for (const key of Object.keys(cookies)) {
registerData.cookies.website_cookies.push({
'Value': cookies[key],
'Name': key
});
}
const options = {
host: `api.${_options.baseAmazonPage}`,
path: '/auth/register',
method: 'POST',
headers: {
'User-Agent': apiCallUserAgent,
'Accept-Language': _options.acceptLanguage,
'Accept-Charset': 'utf-8',
'Connection': 'keep-alive',
'Content-Type': 'application/json',
'Cookie': loginData.loginCookie,
'Accept': 'application/json',
'x-amzn-identity-auth-domain': `api.${_options.baseAmazonPage}`
},
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;
const accessToken = body.response.success.tokens.bearer.access_token;
loginData.tokenDate = Date.now();
loginData.macDms = body.response.success.tokens.mac_dms;
if (body.response.success.tokens.website_cookies && Array.isArray(body.response.success.tokens.website_cookies)) {
const newCookies = [];
body.response.success.tokens.website_cookies.forEach(cookie => {
newCookies.push(`${cookie.Name}=${cookie.Value};`);
});
Cookie = addCookies(Cookie, {'set-cookie': newCookies});
}
registerTokenCapabilities(accessToken, () => {
/*
Get Amazon Marketplace Country
*/
const options = {
host: `alexa.${_options.baseAmazonPage}`,
path: `/api/users/me?platform=ios&version=${apiCallVersion}`,
method: 'GET',
headers: {
'User-Agent': apiCallUserAgent,
'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 || !_options.amazonPage)) {
callback && callback(error, null);
return;
} else if (error && (!_options.formerRegistrationData || !_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;
delete loginData.authorization_code;
delete loginData.verifier;
loginData.dataVersion = 2;
_options.logger && _options.logger(`Final Registration Result: ${JSON.stringify(loginData)}`);
callback && callback(null, loginData);
});
});
});
});
});
};
const registerTokenCapabilities = (accessToken, callback) => {
/*
Register Capabilities - mainly needed for HTTP/2 push infos
*/
const options = {
host: `api.amazonalexa.com`, // How Domains needs to be for other regions? au/jp?
path: `/v1/devices/@self/capabilities`,
method: 'PUT',
headers: {
'User-Agent': apiCallUserAgent,
'Accept-Language': _options.acceptLanguage,
'Accept-Charset': 'utf-8',
'Connection': 'keep-alive',
'Content-type': 'application/json; charset=UTF-8',
'authorization': `Bearer ${accessToken}`,
},
body: '{"legacyFlags":{"SUPPORTS_COMMS":true,"SUPPORTS_ARBITRATION":true,"SCREEN_WIDTH":1170,"SUPPORTS_SCRUBBING":true,"SPEECH_SYNTH_SUPPORTS_TTS_URLS":false,"SUPPORTS_HOME_AUTOMATION":true,"SUPPORTS_DROPIN_OUTBOUND":true,"FRIENDLY_NAME_TEMPLATE":"VOX","SUPPORTS_SIP_OUTBOUND_CALLING":true,"VOICE_PROFILE_SWITCHING_DISABLED":true,"SUPPORTS_LYRICS_IN_CARD":false,"SUPPORTS_DATAMART_NAMESPACE":"Vox","SUPPORTS_VIDEO_CALLING":true,"SUPPORTS_PFM_CHANGED":true,"SUPPORTS_TARGET_PLATFORM":"TABLET","SUPPORTS_SECURE_LOCKSCREEN":false,"AUDIO_PLAYER_SUPPORTS_TTS_URLS":false,"SUPPORTS_KEYS_IN_HEADER":false,"SUPPORTS_MIXING_BEHAVIOR_FOR_AUDIO_PLAYER":false,"AXON_SUPPORT":true,"SUPPORTS_TTS_SPEECHMARKS":true},"envelopeVersion":"20160207","capabilities":[{"version":"0.1","interface":"CardRenderer","type":"AlexaInterface"},{"interface":"Navigation","type":"AlexaInterface","version":"1.1"},{"type":"AlexaInterface","version":"2.0","interface":"Alexa.Comms.PhoneCallController"},{"type":"AlexaInterface","version":"1.1","interface":"ExternalMediaPlayer"},{"type":"AlexaInterface","interface":"Alerts","configurations":{"maximumAlerts":{"timers":2,"overall":99,"alarms":2}},"version":"1.3"},{"version":"1.0","interface":"Alexa.Display.Window","type":"AlexaInterface","configurations":{"templates":[{"type":"STANDARD","id":"app_window_template","configuration":{"sizes":[{"id":"fullscreen","type":"DISCRETE","value":{"value":{"height":1440,"width":3200},"unit":"PIXEL"}}],"interactionModes":["mobile_mode","auto_mode"]}}]}},{"type":"AlexaInterface","interface":"AccessoryKit","version":"0.1"},{"type":"AlexaInterface","interface":"Alexa.AudioSignal.ActiveNoiseControl","version":"1.0","configurations":{"ambientSoundProcessingModes":[{"name":"ACTIVE_NOISE_CONTROL"},{"name":"PASSTHROUGH"}]}},{"interface":"PlaybackController","type":"AlexaInterface","version":"1.0"},{"version":"1.0","interface":"Speaker","type":"AlexaInterface"},{"version":"1.0","interface":"SpeechSynthesizer","type":"AlexaInterface"},{"version":"1.0","interface":"AudioActivityTracker","type":"AlexaInterface"},{"type":"AlexaInterface","interface":"Alexa.Camera.LiveViewController","version":"1.0"},{"type":"AlexaInterface","version":"1.0","interface":"Alexa.Input.Text"},{"type":"AlexaInterface","interface":"Alexa.PlaybackStateReporter","version":"1.0"},{"version":"1.1","interface":"Geolocation","type":"AlexaInterface"},{"interface":"Alexa.Health.Fitness","version":"1.0","type":"AlexaInterface"},{"interface":"Settings","type":"AlexaInterface","version":"1.0"},{"configurations":{"interactionModes":[{"dialog":"SUPPORTED","interactionDistance":{"value":18,"unit":"INCHES"},"video":"SUPPORTED","keyboard":"SUPPORTED","id":"mobile_mode","uiMode":"MOBILE","touch":"SUPPORTED"},{"video":"UNSUPPORTED","dialog":"SUPPORTED","interactionDistance":{"value":36,"unit":"INCHES"},"uiMode":"AUTO","touch":"SUPPORTED","id":"auto_mode","keyboard":"UNSUPPORTED"}]},"type":"AlexaInterface","interface":"Alexa.InteractionMode","version":"1.0"},{"type":"AlexaInterface","configurations":{"catalogs":[{"type":"IOS_APP_STORE","identifierTypes":["URI_HTTP_SCHEME","URI_CUSTOM_SCHEME"]}]},"version":"0.2","interface":"Alexa.Launcher"},{"interface":"System","version":"1.0","type":"AlexaInterface"},{"interface":"Alexa.IOComponents","type":"AlexaInterface","version":"1.4"},{"type":"AlexaInterface","interface":"Alexa.FavoritesController","version":"1.0"},{"version":"1.0","type":"AlexaInterface","interface":"Alexa.Mobile.Push"},{"type":"AlexaInterface","interface":"InteractionModel","version":"1.1"},{"interface":"Alexa.PlaylistController","type":"AlexaInterface","version":"1.0"},{"interface":"SpeechRecognizer","type":"AlexaInterface","version":"2.1"},{"interface":"AudioPlayer","type":"AlexaInterface","version":"1.3"},{"type":"AlexaInterface","version":"3.1","interface":"Alexa.RTCSessionController"},{"interface":"VisualActivityTracker","version":"1.1","type":"AlexaInterface"},{"interface":"Alexa.PlaybackController","version":"1.0","type":"AlexaInterface"},{"type":"AlexaInterface","interface":"Alexa.SeekController","version":"1.0"},{"interface":"Alexa.Comms.MessagingController","type":"AlexaInterface","version":"1.0"}]}'
// New
// {"envelopeVersion":"20160207","legacyFlags":{"SUPPORTS_TARGET_PLATFORM":"TABLET","SUPPORTS_SECURE_LOCKSCREEN":false,"SUPPORTS_DATAMART_NAMESPACE":"Vox","AXON_SUPPORT":true,"SUPPORTS_DROPIN_OUTBOUND":true,"SUPPORTS_LYRICS_IN_CARD":false,"VOICE_PROFILE_SWITCHING_DISABLED":true,"SUPPORTS_ARBITRATION":true,"SUPPORTS_HOME_AUTOMATION":true,"SUPPORTS_KEYS_IN_HEADER":false,"SUPPORTS_TTS_SPEECHMARKS":true,"AUDIO_PLAYER_SUPPORTS_TTS_URLS":false,"SUPPORTS_SIP_OUTBOUND_CALLING":true,"SUPPORTS_MIXING_BEHAVIOR_FOR_AUDIO_PLAYER":false,"SUPPORTS_COMMS":true,"SCREEN_WIDTH":1170,"SUPPORTS_VIDEO_CALLING":true,"FRIENDLY_NAME_TEMPLATE":"VOX","SUPPORTS_PFM_CHANGED":true,"SPEECH_SYNTH_SUPPORTS_TTS_URLS":false,"SUPPORTS_SCRUBBING":true},"capabilities":[{"type":"AlexaInterface","interface":"AudioPlayer","version":"1.3"},{"version":"1.0","type":"AlexaInterface","interface":"Settings"},{"interface":"System","type":"AlexaInterface","version":"1.0"},{"type":"AlexaInterface","interface":"AudioActivityTracker","version":"1.0"},{"interface":"SpeechRecognizer","version":"2.3","type":"AlexaInterface"},{"type":"AlexaInterface","interface":"Speaker","version":"1.0"},{"type":"AlexaInterface","version":"1.0","interface":"SpeechSynthesizer"},{"type":"AlexaInterface","version":"0.1","interface":"CardRenderer"},{"interface":"PlaybackController","type":"AlexaInterface","version":"1.0"},{"version":"1.1","type":"AlexaInterface","interface":"Navigation"},{"version":"1.1","type":"AlexaInterface","interface":"InteractionModel"},{"type":"AlexaInterface","version":"1.1","interface":"Geolocation"}]}
};
_options.logger && _options.logger('Alexa-Cookie: Register capabilities');
_options.logger && _options.logger(JSON.stringify(options));
request(options, (error, response, body) => {
if (error || (response.statusCode !== 204 && response.statusCode !== 200)) {
_options.logger && _options.logger('Alexa-Cookie: Could not set capabilities, Push connection might not work!');
_options.logger && _options.logger(`Alexa-Cookie: ${JSON.stringify(error)}: ${JSON.stringify(body)}`);
}
callback && callback();
});
};
const 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': apiCallVersion,
'domain': `.${amazonPage}`,
'source_token': refreshToken,
'requested_token_type': 'auth_cookies',
'source_token_type': 'refresh_token',
'di.hw.version': 'iPhone',
'di.sdk.version': '6.12.4',
'app_name': _options.deviceAppName || defaultAppName,
'di.os.version': '16.6'
};
const options = {
host: `www.${amazonPage}`,
path: '/ap/exchangetoken/cookies',
method: 'POST',
headers: {
'User-Agent': apiCallUserAgent,
'Accept-Language': _options.acceptLanguage,
'Accept-Charset': 'utf-8',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': '*/*',
'Cookie': Cookie,
'x-amzn-identity-auth-domain': `api.${amazonPage}`
},
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 (const name of Object.keys(cookies)) {
localCookie += `${name}=${cookies[name]}; `;
}
localCookie = localCookie.replace(/[; ]*$/, '');
callback && callback(null, localCookie);
});
};
this.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': _options.deviceAppName || defaultAppName,
'app_version': apiCallVersion,
'di.sdk.version': '6.12.4',
'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': '16.6',
'current_version': '6.12.4'
};
const options = {
host: `api.${_options.baseAmazonPage}`,
path: '/auth/token',
method: 'POST',
headers: {
'User-Agent': apiCallUserAgent,
'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.${_options.baseAmazonPage}`
},
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(_options.baseAmazonPage, _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);
});
});
};
this.stopProxyServer = (callback) => {
if (proxyServer) {
proxyServer.close(() => {
callback && callback();
});
}
proxyServer = null;
};
}
module.exports = AlexaCookie();