alexa-cookie2 5.0.3 → 5.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +6 -0
- package/alexa-cookie.js +80 -17
- package/lib/proxy.js +134 -45
- package/package.json +5 -5
- package/test/base-amazon-page-handle.test.js +123 -0
- package/test/proxy-cookie-rewrite.test.js +148 -0
- package/test/proxy-cookie-sanitizing.test.js +186 -0
- package/test/proxy-host-switch.test.js +166 -0
- package/test/proxy-initial-url.test.js +184 -0
- package/test/proxy-random-port.test.js +147 -0
- package/test/proxy-success-requires-auth-code.test.js +170 -0
- package/test/refresh-skips-app-registration.test.js +270 -0
- package/test/registration-hardening.test.js +261 -0
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2018-
|
|
3
|
+
Copyright (c) 2018-2026 Apollon77 <ingo@fischer-ka.de>
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -55,6 +55,12 @@ Partly based on [Amazon Alexa Remote Control](http://blog.loetzimmer.de/2017/10/
|
|
|
55
55
|
Thank you for that work.
|
|
56
56
|
|
|
57
57
|
## Changelog:
|
|
58
|
+
### 5.0.5 (2026-07-06)
|
|
59
|
+
* (@Apollon77) Fix Proxy URL when no static port is provided
|
|
60
|
+
|
|
61
|
+
### 5.0.4 (2026-07-05)
|
|
62
|
+
* (@fkhr79, @blabond) Fix Amazon login proxy auth flow
|
|
63
|
+
|
|
58
64
|
### 5.0.3 (2025-07-13)
|
|
59
65
|
* (Apollon77) Update some version details to sync with Alexa App
|
|
60
66
|
|
package/alexa-cookie.js
CHANGED
|
@@ -39,6 +39,19 @@ function AlexaCookie() {
|
|
|
39
39
|
|
|
40
40
|
let Cookie = '';
|
|
41
41
|
|
|
42
|
+
const isAmazonCookieName = name => /^(session-id|session-id-time|session-token|ubid-.+|lc-.+|x-.+|at-.+|sess-at-.+|frc|map-md|csrf|sid|csm-hit|i18n-prefs|sp-cdn|skin)$/.test(name);
|
|
43
|
+
|
|
44
|
+
const sanitizeAmazonCookie = cookie => {
|
|
45
|
+
const cookies = cookieTools.parse(cookie || '');
|
|
46
|
+
let sanitizedCookie = '';
|
|
47
|
+
for (const name of Object.keys(cookies)) {
|
|
48
|
+
if (isAmazonCookieName(name)) {
|
|
49
|
+
sanitizedCookie += `${name}=${cookies[name]}; `;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return sanitizedCookie.replace(/[; ]*$/, '');
|
|
53
|
+
};
|
|
54
|
+
|
|
42
55
|
const addCookies = (Cookie, headers) => {
|
|
43
56
|
if (!headers || !headers['set-cookie']) return Cookie;
|
|
44
57
|
const cookies = cookieTools.parse(Cookie || '');
|
|
@@ -155,8 +168,7 @@ function AlexaCookie() {
|
|
|
155
168
|
_options.baseAmazonPageHandle = `_${amazonDomain}`;
|
|
156
169
|
}
|
|
157
170
|
else if (amazonDomain !== 'com') {
|
|
158
|
-
|
|
159
|
-
_options.baseAmazonPageHandle = '';
|
|
171
|
+
_options.baseAmazonPageHandle = `_${amazonDomain}`;
|
|
160
172
|
}
|
|
161
173
|
else {
|
|
162
174
|
_options.baseAmazonPageHandle = '';
|
|
@@ -408,6 +420,7 @@ function AlexaCookie() {
|
|
|
408
420
|
_options.logger && _options.logger(`Handle token registration Start: ${JSON.stringify(loginData)}`);
|
|
409
421
|
|
|
410
422
|
loginData.deviceAppName = _options.deviceAppName;
|
|
423
|
+
loginData.loginCookie = sanitizeAmazonCookie(loginData.loginCookie);
|
|
411
424
|
|
|
412
425
|
let deviceSerial;
|
|
413
426
|
if (!_options.formerRegistrationData || !_options.formerRegistrationData.deviceSerial) {
|
|
@@ -520,6 +533,7 @@ function AlexaCookie() {
|
|
|
520
533
|
Cookie = addCookies(Cookie, response.headers);
|
|
521
534
|
loginData.refreshToken = body.response.success.tokens.bearer.refresh_token;
|
|
522
535
|
const accessToken = body.response.success.tokens.bearer.access_token;
|
|
536
|
+
loginData.accessToken = accessToken;
|
|
523
537
|
loginData.tokenDate = Date.now();
|
|
524
538
|
loginData.macDms = body.response.success.tokens.mac_dms;
|
|
525
539
|
|
|
@@ -553,22 +567,34 @@ function AlexaCookie() {
|
|
|
553
567
|
_options.logger && _options.logger(JSON.stringify(options));
|
|
554
568
|
request(options, (error, response, body) => {
|
|
555
569
|
if (!error) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
570
|
+
if (!response || response.statusCode < 200 || response.statusCode >= 300 || !body) {
|
|
571
|
+
_options.logger && _options.logger(`Get User data Response: status=${response && response.statusCode}, body=${JSON.stringify(body)}`);
|
|
572
|
+
if (!_options.amazonPage) {
|
|
573
|
+
callback && callback(new Error(`Could not get user data from Amazon: HTTP ${response && response.statusCode}`), null);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
loginData.amazonPage = _options.amazonPage;
|
|
577
|
+
} else {
|
|
578
|
+
try {
|
|
579
|
+
if (typeof body !== 'object') body = JSON.parse(body);
|
|
580
|
+
} catch (err) {
|
|
581
|
+
_options.logger && _options.logger(`Get User data Response: ${JSON.stringify(body)}`);
|
|
582
|
+
if (!_options.amazonPage) {
|
|
583
|
+
callback && callback(err, null);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
loginData.amazonPage = _options.amazonPage;
|
|
587
|
+
}
|
|
559
588
|
_options.logger && _options.logger(`Get User data Response: ${JSON.stringify(body)}`);
|
|
560
|
-
callback && callback(err, null);
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
_options.logger && _options.logger(`Get User data Response: ${JSON.stringify(body)}`);
|
|
564
589
|
|
|
565
|
-
|
|
590
|
+
Cookie = addCookies(Cookie, response.headers);
|
|
566
591
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
592
|
+
if (body && body.marketPlaceDomainName) {
|
|
593
|
+
const pos = body.marketPlaceDomainName.indexOf('.');
|
|
594
|
+
if (pos !== -1) _options.amazonPage = body.marketPlaceDomainName.substr(pos + 1);
|
|
595
|
+
}
|
|
596
|
+
loginData.amazonPage = _options.amazonPage;
|
|
570
597
|
}
|
|
571
|
-
loginData.amazonPage = _options.amazonPage;
|
|
572
598
|
} else if (error && (!_options || !_options.amazonPage)) {
|
|
573
599
|
callback && callback(error, null);
|
|
574
600
|
return;
|
|
@@ -583,6 +609,7 @@ function AlexaCookie() {
|
|
|
583
609
|
getLocalCookies(loginData.amazonPage, loginData.refreshToken, (err, localCookie) => {
|
|
584
610
|
if (err) {
|
|
585
611
|
callback && callback(err, null);
|
|
612
|
+
return;
|
|
586
613
|
}
|
|
587
614
|
|
|
588
615
|
loginData.localCookie = localCookie;
|
|
@@ -593,7 +620,6 @@ function AlexaCookie() {
|
|
|
593
620
|
}
|
|
594
621
|
loginData.localCookie = resData.cookie;
|
|
595
622
|
loginData.csrf = resData.csrf;
|
|
596
|
-
delete loginData.accessToken;
|
|
597
623
|
delete loginData.authorization_code;
|
|
598
624
|
delete loginData.verifier;
|
|
599
625
|
loginData.dataVersion = 2;
|
|
@@ -721,6 +747,42 @@ function AlexaCookie() {
|
|
|
721
747
|
});
|
|
722
748
|
};
|
|
723
749
|
|
|
750
|
+
const finishCookieRefresh = (loginData, callback) => {
|
|
751
|
+
_options.logger && _options.logger('Alexa-Cookie: Skip App registration during refresh and update local cookies');
|
|
752
|
+
const updateLocalCookies = () => {
|
|
753
|
+
const amazonPage = loginData.amazonPage || _options.amazonPage;
|
|
754
|
+
getLocalCookies(amazonPage, loginData.refreshToken, (err, localCookie) => {
|
|
755
|
+
if (err) {
|
|
756
|
+
callback && callback(err, null);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
loginData.localCookie = localCookie;
|
|
761
|
+
getCSRFFromCookies(loginData.localCookie, _options, (err, resData) => {
|
|
762
|
+
if (err) {
|
|
763
|
+
callback && callback(new Error(`Error getting csrf for ${amazonPage}`), null);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
loginData.localCookie = resData.cookie;
|
|
767
|
+
loginData.csrf = resData.csrf;
|
|
768
|
+
loginData.amazonPage = amazonPage;
|
|
769
|
+
loginData.tokenDate = Date.now();
|
|
770
|
+
delete loginData.authorization_code;
|
|
771
|
+
delete loginData.verifier;
|
|
772
|
+
loginData.dataVersion = 2;
|
|
773
|
+
_options.logger && _options.logger('Alexa-Cookie: Refresh finished with updated cookies and csrf');
|
|
774
|
+
callback && callback(null, loginData);
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
if (loginData.accessToken) {
|
|
780
|
+
registerTokenCapabilities(loginData.accessToken, updateLocalCookies);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
updateLocalCookies();
|
|
784
|
+
};
|
|
785
|
+
|
|
724
786
|
this.refreshAlexaCookie = (__options, callback) => {
|
|
725
787
|
if (!__options || !__options.formerRegistrationData || !__options.formerRegistrationData.loginCookie || !__options.formerRegistrationData.refreshToken) {
|
|
726
788
|
callback && callback(new Error('No former registration data provided for Cookie Refresh'), null);
|
|
@@ -798,6 +860,7 @@ function AlexaCookie() {
|
|
|
798
860
|
getLocalCookies(_options.baseAmazonPage, _options.formerRegistrationData.refreshToken, (err, comCookie) => {
|
|
799
861
|
if (err) {
|
|
800
862
|
callback && callback(err, null);
|
|
863
|
+
return;
|
|
801
864
|
}
|
|
802
865
|
|
|
803
866
|
// Restore frc and map-md
|
|
@@ -806,8 +869,8 @@ function AlexaCookie() {
|
|
|
806
869
|
newCookie += `map-md=${initCookies['map-md']}; `;
|
|
807
870
|
newCookie += comCookie;
|
|
808
871
|
|
|
809
|
-
_options.formerRegistrationData.loginCookie = newCookie;
|
|
810
|
-
|
|
872
|
+
_options.formerRegistrationData.loginCookie = sanitizeAmazonCookie(newCookie);
|
|
873
|
+
finishCookieRefresh(_options.formerRegistrationData, callback);
|
|
811
874
|
});
|
|
812
875
|
});
|
|
813
876
|
};
|
package/lib/proxy.js
CHANGED
|
@@ -15,6 +15,8 @@ const path = require('path');
|
|
|
15
15
|
const crypto = require('crypto');
|
|
16
16
|
|
|
17
17
|
const FORMERDATA_STORE_VERSION = 4;
|
|
18
|
+
const ALEXA_APP_VERSION = '2.2.651540.0';
|
|
19
|
+
const EXTRA_AMAZON_PROXY_HOSTS = ['amazon.com', 'www.amazon.com', 'alexa.amazon.com'];
|
|
18
20
|
|
|
19
21
|
function addCookies(Cookie, headers) {
|
|
20
22
|
if (!headers || !headers['set-cookie']) return Cookie;
|
|
@@ -34,6 +36,21 @@ function addCookies(Cookie, headers) {
|
|
|
34
36
|
return Cookie;
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
function isAmazonCookieName(name) {
|
|
40
|
+
return /^(session-id|session-id-time|session-token|ubid-.+|lc-.+|x-.+|at-.+|sess-at-.+|frc|map-md|csrf|sid|csm-hit|i18n-prefs|sp-cdn|skin)$/.test(name);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sanitizeAmazonCookie(cookie) {
|
|
44
|
+
const cookies = cookieTools.parse(cookie || '');
|
|
45
|
+
let sanitizedCookie = '';
|
|
46
|
+
for (const name of Object.keys(cookies)) {
|
|
47
|
+
if (isAmazonCookieName(name)) {
|
|
48
|
+
sanitizedCookie += `${name}=${cookies[name]}; `;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return sanitizedCookie.replace(/[; ]*$/, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
37
54
|
function customStringify(v, func, intent) {
|
|
38
55
|
const cache = new Map();
|
|
39
56
|
return JSON.stringify(v, function (key, value) {
|
|
@@ -55,6 +72,46 @@ function customStringify(v, func, intent) {
|
|
|
55
72
|
|
|
56
73
|
function initAmazonProxy(_options, callbackCookie, callbackListening) {
|
|
57
74
|
const initialCookies = {};
|
|
75
|
+
const proxyBase = () => `http://${_options.proxyOwnIp}:${_options.proxyPort}/`;
|
|
76
|
+
const defaultAmazonHost = `www.${_options.baseAmazonPage}`;
|
|
77
|
+
const amazonProxyHosts = [];
|
|
78
|
+
|
|
79
|
+
function escapeRegex(data) {
|
|
80
|
+
return data.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function addAmazonProxyHost(host) {
|
|
84
|
+
if (host && !amazonProxyHosts.includes(host)) {
|
|
85
|
+
amazonProxyHosts.push(host);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function amazonRootFromHost(host) {
|
|
90
|
+
return host.replace(/^(www|alexa)\./, '');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
addAmazonProxyHost(defaultAmazonHost);
|
|
94
|
+
addAmazonProxyHost(`alexa.${_options.baseAmazonPage}`);
|
|
95
|
+
addAmazonProxyHost(_options.baseAmazonPage);
|
|
96
|
+
EXTRA_AMAZON_PROXY_HOSTS.forEach(addAmazonProxyHost);
|
|
97
|
+
|
|
98
|
+
function amazonHostFromProxyPath(url) {
|
|
99
|
+
const match = (url || '').match(/^\/([^/?#]+)(?:[/?#]|$)/);
|
|
100
|
+
if (match && amazonProxyHosts.includes(match[1])) {
|
|
101
|
+
return match[1];
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function amazonHostFromProxyUrl(url) {
|
|
107
|
+
if (!url) return null;
|
|
108
|
+
for (const host of amazonProxyHosts) {
|
|
109
|
+
if (url === `${proxyBase()}${host}` || url.startsWith(`${proxyBase()}${host}/`)) {
|
|
110
|
+
return host;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
58
115
|
|
|
59
116
|
const formerDataStorePath = _options.formerDataStorePath || path.join(__dirname, 'formerDataStore.json');
|
|
60
117
|
let formerDataStoreValid = false;
|
|
@@ -90,7 +147,7 @@ function initAmazonProxy(_options, callbackCookie, callbackListening) {
|
|
|
90
147
|
}
|
|
91
148
|
|
|
92
149
|
if (!_options.formerRegistrationData || !_options.formerRegistrationData['map-md']) {
|
|
93
|
-
initialCookies['map-md'] = Buffer.from(
|
|
150
|
+
initialCookies['map-md'] = Buffer.from(`{"device_user_dictionary":[],"device_registration_data":{"software_version":"1"},"app_identifier":{"app_version":"${ALEXA_APP_VERSION}","bundle_id":"com.amazon.echo"}}`).toString('base64');
|
|
94
151
|
}
|
|
95
152
|
else {
|
|
96
153
|
_options.logger && _options.logger('Proxy Init: reuse map-md from former data');
|
|
@@ -135,13 +192,31 @@ function initAmazonProxy(_options, callbackCookie, callbackListening) {
|
|
|
135
192
|
const code_challenge = base64URLEncode(sha256(code_verifier));
|
|
136
193
|
|
|
137
194
|
let proxyCookies = '';
|
|
195
|
+
let returnedInitUrl;
|
|
196
|
+
|
|
197
|
+
function buildInitialUrl() {
|
|
198
|
+
return `https://www.${_options.baseAmazonPage}/ap/signin?openid.return_to=https%3A%2F%2Fwww.${_options.baseAmazonPage}%2Fap%2Fmaplanding&openid.assoc_handle=amzn_dp_project_dee_ios${_options.baseAmazonPageHandle}&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&pageId=amzn_dp_project_dee_ios${_options.baseAmazonPageHandle}&accountStatusPolicy=P1&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.mode=checkid_setup&openid.ns.oa2=http%3A%2F%2Fwww.${_options.baseAmazonPage}%2Fap%2Fext%2Foauth%2F2&openid.oa2.client_id=device%3A${deviceId}&openid.ns.pape=http%3A%2F%2Fspecs.openid.net%2Fextensions%2Fpape%2F1.0&openid.oa2.response_type=code&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.pape.max_auth_age=0&openid.oa2.scope=device_auth_access&openid.oa2.code_challenge_method=S256&openid.oa2.code_challenge=${code_challenge}&language=${_options.amazonPageProxyLanguage}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function rewriteProxyPath(url, req) {
|
|
202
|
+
const urlHost = amazonHostFromProxyPath(url);
|
|
203
|
+
if (urlHost) {
|
|
204
|
+
return url.replace(new RegExp(`^/${escapeRegex(urlHost)}`), '') || '/';
|
|
205
|
+
}
|
|
206
|
+
if (req && req.headers && req.headers.host === `${_options.proxyOwnIp}:${_options.proxyPort}` && url === '/') {
|
|
207
|
+
const initialUrl = returnedInitUrl || buildInitialUrl();
|
|
208
|
+
const parsed = new URL(initialUrl);
|
|
209
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
210
|
+
}
|
|
211
|
+
return url;
|
|
212
|
+
}
|
|
138
213
|
|
|
139
214
|
// proxy middleware options
|
|
140
215
|
const optionsAlexa = {
|
|
141
216
|
target: `https://alexa.${_options.baseAmazonPage}`,
|
|
142
217
|
changeOrigin: true,
|
|
143
218
|
ws: false,
|
|
144
|
-
pathRewrite:
|
|
219
|
+
pathRewrite: rewriteProxyPath,
|
|
145
220
|
router: router,
|
|
146
221
|
hostRewrite: true,
|
|
147
222
|
followRedirects: false,
|
|
@@ -150,18 +225,15 @@ function initAmazonProxy(_options, callbackCookie, callbackListening) {
|
|
|
150
225
|
onProxyRes: onProxyRes,
|
|
151
226
|
onProxyReq: onProxyReq,
|
|
152
227
|
headers: {
|
|
153
|
-
'
|
|
154
|
-
'accept-language': _options.acceptLanguage,
|
|
155
|
-
'authority': `www.${_options.baseAmazonPage}`
|
|
228
|
+
'accept-language': _options.acceptLanguage
|
|
156
229
|
},
|
|
157
|
-
cookieDomainRewrite: {
|
|
230
|
+
cookieDomainRewrite: {
|
|
158
231
|
'*': ''
|
|
232
|
+
},
|
|
233
|
+
cookiePathRewrite: {
|
|
234
|
+
'*': '/'
|
|
159
235
|
}
|
|
160
236
|
};
|
|
161
|
-
optionsAlexa.pathRewrite[`^/www.${_options.baseAmazonPage}`] = '';
|
|
162
|
-
optionsAlexa.pathRewrite[`^/alexa.${_options.baseAmazonPage}`] = '';
|
|
163
|
-
optionsAlexa.cookieDomainRewrite[`.${_options.baseAmazonPage}`] = _options.proxyOwnIp;
|
|
164
|
-
optionsAlexa.cookieDomainRewrite[_options.baseAmazonPage] = _options.proxyOwnIp;
|
|
165
237
|
if (_options.logger) optionsAlexa.logProvider = function logProvider() {
|
|
166
238
|
return {
|
|
167
239
|
log: _options.logger.log || _options.logger,
|
|
@@ -171,30 +243,27 @@ function initAmazonProxy(_options, callbackCookie, callbackListening) {
|
|
|
171
243
|
error: _options.logger.error || _options.logger
|
|
172
244
|
};
|
|
173
245
|
};
|
|
174
|
-
let returnedInitUrl;
|
|
175
246
|
|
|
176
247
|
function router(req) {
|
|
177
248
|
const url = (req.originalUrl || req.url);
|
|
178
249
|
_options.logger && _options.logger(`Router: ${url} / ${req.method} / ${JSON.stringify(req.headers)}`);
|
|
179
250
|
if (req.headers.host === `${_options.proxyOwnIp}:${_options.proxyPort}`) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
} else if (req.headers.referer.startsWith(`http://${_options.proxyOwnIp}:${_options.proxyPort}/alexa.${_options.baseAmazonPage}/`)) {
|
|
188
|
-
return `https://alexa.${_options.baseAmazonPage}`;
|
|
189
|
-
}
|
|
251
|
+
const urlHost = amazonHostFromProxyPath(url);
|
|
252
|
+
if (urlHost) {
|
|
253
|
+
return `https://${urlHost}`;
|
|
254
|
+
}
|
|
255
|
+
const refererHost = amazonHostFromProxyUrl(req.headers.referer);
|
|
256
|
+
if (refererHost) {
|
|
257
|
+
return `https://${refererHost}`;
|
|
190
258
|
}
|
|
191
259
|
if (url === '/') { // initial redirect
|
|
192
|
-
returnedInitUrl =
|
|
260
|
+
returnedInitUrl = buildInitialUrl();
|
|
193
261
|
_options.logger && _options.logger(`Alexa-Cookie: Initial Page Request: ${returnedInitUrl}`);
|
|
194
|
-
|
|
262
|
+
const parsedInitUrl = new URL(returnedInitUrl);
|
|
263
|
+
return `${parsedInitUrl.protocol}//${parsedInitUrl.host}`;
|
|
195
264
|
}
|
|
196
265
|
else {
|
|
197
|
-
return `https
|
|
266
|
+
return `https://${defaultAmazonHost}`;
|
|
198
267
|
}
|
|
199
268
|
}
|
|
200
269
|
return `https://alexa.${_options.baseAmazonPage}`;
|
|
@@ -214,26 +283,43 @@ function initAmazonProxy(_options, callbackCookie, callbackListening) {
|
|
|
214
283
|
|
|
215
284
|
function replaceHosts(data) {
|
|
216
285
|
//const dataOrig = data;
|
|
217
|
-
const amazonRegex = new RegExp(`https?://www.${_options.baseAmazonPage}:?[0-9]*/`.replace(/\./g, '\\.'), 'g');
|
|
218
|
-
const alexaRegex = new RegExp(`https?://alexa.${_options.baseAmazonPage}:?[0-9]*/`.replace(/\./g, '\\.'), 'g');
|
|
219
286
|
data = data.replace(///g, '/');
|
|
220
|
-
|
|
221
|
-
|
|
287
|
+
for (const host of amazonProxyHosts) {
|
|
288
|
+
const hostRegex = new RegExp(`https?://${escapeRegex(host)}:?[0-9]*/`, 'g');
|
|
289
|
+
data = data.replace(hostRegex, `${proxyBase()}${host}/`);
|
|
290
|
+
}
|
|
222
291
|
//_options.logger && _options.logger('REPLACEHOSTS: ' + dataOrig + ' --> ' + data);
|
|
223
292
|
return data;
|
|
224
293
|
}
|
|
225
294
|
|
|
226
295
|
function replaceHostsBack(data) {
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
296
|
+
const base = proxyBase();
|
|
297
|
+
for (const host of amazonProxyHosts) {
|
|
298
|
+
const hostRegex = new RegExp(`${escapeRegex(base)}${escapeRegex(host)}/`, 'g');
|
|
299
|
+
data = data.replace(hostRegex, `https://${host}/`);
|
|
300
|
+
}
|
|
301
|
+
if (data === base) {
|
|
232
302
|
data = returnedInitUrl;
|
|
233
303
|
}
|
|
304
|
+
else if (data.startsWith(base)) {
|
|
305
|
+
data = `https://${defaultAmazonHost}/${data.slice(base.length)}`;
|
|
306
|
+
}
|
|
234
307
|
return data;
|
|
235
308
|
}
|
|
236
309
|
|
|
310
|
+
function parseQueryParams(data) {
|
|
311
|
+
const paramStart = data && data.indexOf('?');
|
|
312
|
+
if (paramStart === -1 || paramStart === undefined) return {};
|
|
313
|
+
return querystring.parse(data.substr(paramStart + 1));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isProxySuccessUrl(data) {
|
|
317
|
+
if (!data) return false;
|
|
318
|
+
if (data.includes('/spa/index.html')) return true;
|
|
319
|
+
if (!data.includes('/ap/maplanding?')) return false;
|
|
320
|
+
return !!parseQueryParams(data)['openid.oa2.authorization_code'];
|
|
321
|
+
}
|
|
322
|
+
|
|
237
323
|
function onProxyReq(proxyReq, req/*, _res*/) {
|
|
238
324
|
const url = req.originalUrl || req.url;
|
|
239
325
|
if (url.endsWith('.ico') || url.endsWith('.js') || url.endsWith('.ttf') || url.endsWith('.svg') || url.endsWith('.png') || url.endsWith('.appcache')) return;
|
|
@@ -276,8 +362,8 @@ function initAmazonProxy(_options, callbackCookie, callbackListening) {
|
|
|
276
362
|
}
|
|
277
363
|
}
|
|
278
364
|
if (typeof proxyReq.getHeader === 'function' && proxyReq.getHeader('origin') !== `https://${proxyReq.getHeader('host')}`) {
|
|
279
|
-
proxyReq.setHeader('origin', `https
|
|
280
|
-
_options.logger && _options.logger('Alexa-Cookie: Modify headers:
|
|
365
|
+
proxyReq.setHeader('origin', `https://${proxyReq.getHeader('host') || defaultAmazonHost}`);
|
|
366
|
+
_options.logger && _options.logger('Alexa-Cookie: Modify headers: Changed Origin');
|
|
281
367
|
modified = true;
|
|
282
368
|
}
|
|
283
369
|
|
|
@@ -310,15 +396,17 @@ function initAmazonProxy(_options, callbackCookie, callbackListening) {
|
|
|
310
396
|
}
|
|
311
397
|
_options.logger && _options.logger(`Alexa-Cookie: Cookies handled: ${JSON.stringify(proxyCookies)}`);
|
|
312
398
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
399
|
+
const outgoing = proxyRes.socket && proxyRes.socket.parser && proxyRes.socket.parser.outgoing;
|
|
400
|
+
const successUrl = [
|
|
401
|
+
proxyRes.headers.location,
|
|
402
|
+
outgoing && outgoing.method === 'GET' && outgoing.path,
|
|
403
|
+
outgoing && outgoing.getHeader && outgoing.getHeader('location')
|
|
404
|
+
].find(isProxySuccessUrl);
|
|
405
|
+
|
|
406
|
+
if (successUrl) {
|
|
318
407
|
_options.logger && _options.logger('Alexa-Cookie: Proxy detected SUCCESS!!');
|
|
319
408
|
|
|
320
|
-
const
|
|
321
|
-
const queryParams = querystring.parse(proxyRes.headers.location.substr(paramStart + 1));
|
|
409
|
+
const queryParams = parseQueryParams(successUrl);
|
|
322
410
|
|
|
323
411
|
proxyRes.statusCode = 302;
|
|
324
412
|
proxyRes.headers.location = `http://${_options.proxyOwnIp}:${_options.proxyPort}/cookie-success`;
|
|
@@ -328,7 +416,7 @@ function initAmazonProxy(_options, callbackCookie, callbackListening) {
|
|
|
328
416
|
_options.logger && _options.logger(`Alexa-Cookie: Proxy catched parameters: ${JSON.stringify(queryParams)}`);
|
|
329
417
|
|
|
330
418
|
callbackCookie && callbackCookie(null, {
|
|
331
|
-
'loginCookie': proxyCookies,
|
|
419
|
+
'loginCookie': sanitizeAmazonCookie(proxyCookies),
|
|
332
420
|
'authorization_code': queryParams['openid.oa2.authorization_code'],
|
|
333
421
|
'frc': initialCookies.frc,
|
|
334
422
|
'map-md': initialCookies['map-md'],
|
|
@@ -342,7 +430,7 @@ function initAmazonProxy(_options, callbackCookie, callbackListening) {
|
|
|
342
430
|
if (proxyRes.headers.location) {
|
|
343
431
|
_options.logger && _options.logger(`Redirect: Original Location ----> ${proxyRes.headers.location}`);
|
|
344
432
|
proxyRes.headers.location = replaceHosts(proxyRes.headers.location);
|
|
345
|
-
if (reqestHost && proxyRes.headers.location.startsWith('/')) {
|
|
433
|
+
if (reqestHost && amazonProxyHosts.includes(reqestHost) && proxyRes.headers.location.startsWith('/')) {
|
|
346
434
|
proxyRes.headers.location = `http://${_options.proxyOwnIp}:${_options.proxyPort}/${reqestHost}${proxyRes.headers.location}`;
|
|
347
435
|
}
|
|
348
436
|
_options.logger && _options.logger(`Redirect: Final Location ----> ${proxyRes.headers.location}`);
|
|
@@ -379,7 +467,8 @@ function initAmazonProxy(_options, callbackCookie, callbackListening) {
|
|
|
379
467
|
_options.proxyPort = undefined;
|
|
380
468
|
}
|
|
381
469
|
const server = app.listen(_options.proxyPort, _options.proxyListenBind, function() {
|
|
382
|
-
_options.
|
|
470
|
+
_options.proxyPort = this.address().port;
|
|
471
|
+
_options.logger && _options.logger(`Alexa-Cookie: Proxy-Server listening on port ${_options.proxyPort}`);
|
|
383
472
|
callbackListening && callbackListening(server);
|
|
384
473
|
callbackListening = null;
|
|
385
474
|
}).on('error', err => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alexa-cookie2",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.5",
|
|
4
4
|
"description": "Generate Cookie and CSRF for Alexa Remote",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Apollon77",
|
|
@@ -22,15 +22,15 @@
|
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"cookie": "^0.6.0",
|
|
25
|
-
"express": "^4.
|
|
26
|
-
"http-proxy-middleware": "^2.0.
|
|
25
|
+
"express": "^4.22.2",
|
|
26
|
+
"http-proxy-middleware": "^2.0.10",
|
|
27
27
|
"http-proxy-response-rewrite": "^0.0.1",
|
|
28
28
|
"https": "^1.0.0",
|
|
29
29
|
"querystring": "^0.2.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@alcalzone/release-script": "^
|
|
33
|
-
"@alcalzone/release-script-plugin-license": "^
|
|
32
|
+
"@alcalzone/release-script": "^5.2.1",
|
|
33
|
+
"@alcalzone/release-script-plugin-license": "^5.2.0",
|
|
34
34
|
"eslint": "^8.57.1"
|
|
35
35
|
},
|
|
36
36
|
"repository": {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const vm = require('vm');
|
|
5
|
+
|
|
6
|
+
const testName = 'base-amazon-page-handle';
|
|
7
|
+
const outputDir = process.env.ALEXA_COOKIE_TEST_OUTPUT_DIR || path.join(__dirname, '..', 'test-output');
|
|
8
|
+
const outputFile = path.join(outputDir, `${testName}.txt`);
|
|
9
|
+
const lines = [];
|
|
10
|
+
|
|
11
|
+
function line(value = '') {
|
|
12
|
+
lines.push(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writeOutput() {
|
|
16
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
17
|
+
fs.writeFileSync(outputFile, `${lines.join('\n')}\n`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function recordAssertion(description, fn) {
|
|
21
|
+
try {
|
|
22
|
+
fn();
|
|
23
|
+
line(`${description}: PASS`);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
line(`${description}: FAIL`);
|
|
26
|
+
writeOutput();
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const cookieFile = path.join(__dirname, '..', 'alexa-cookie.js');
|
|
32
|
+
const source = fs.readFileSync(cookieFile, 'utf8');
|
|
33
|
+
|
|
34
|
+
let capturedProxyOptions;
|
|
35
|
+
|
|
36
|
+
function loadCookieModule() {
|
|
37
|
+
capturedProxyOptions = undefined;
|
|
38
|
+
const module = { exports: {} };
|
|
39
|
+
const sandbox = {
|
|
40
|
+
__dirname: path.dirname(cookieFile),
|
|
41
|
+
console,
|
|
42
|
+
module,
|
|
43
|
+
exports: module.exports,
|
|
44
|
+
require(name) {
|
|
45
|
+
if (name === './lib/proxy.js') {
|
|
46
|
+
return {
|
|
47
|
+
initAmazonProxy(options, _callbackCookie, callbackListening) {
|
|
48
|
+
capturedProxyOptions = options;
|
|
49
|
+
callbackListening && callbackListening({
|
|
50
|
+
address: () => ({ port: options.proxyPort || 3456 }),
|
|
51
|
+
close: (callback) => callback && callback()
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (name === 'cookie') {
|
|
57
|
+
return { parse: () => ({}) };
|
|
58
|
+
}
|
|
59
|
+
return require(name);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
vm.runInNewContext(source, sandbox, { filename: cookieFile });
|
|
63
|
+
return module.exports;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runProxyOnlyConfig(baseAmazonPage) {
|
|
67
|
+
const cookieModule = loadCookieModule();
|
|
68
|
+
const input = {
|
|
69
|
+
proxyOwnIp: '127.0.0.1',
|
|
70
|
+
proxyPort: 3456,
|
|
71
|
+
baseAmazonPage,
|
|
72
|
+
amazonPage: baseAmazonPage,
|
|
73
|
+
logger: () => {}
|
|
74
|
+
};
|
|
75
|
+
cookieModule.generateAlexaCookie('', '', input, () => {});
|
|
76
|
+
return { input, output: capturedProxyOptions };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const de = runProxyOnlyConfig('amazon.de');
|
|
81
|
+
const uk = runProxyOnlyConfig('amazon.co.uk');
|
|
82
|
+
const com = runProxyOnlyConfig('amazon.com');
|
|
83
|
+
|
|
84
|
+
line('TEST: base Amazon page handle');
|
|
85
|
+
line('');
|
|
86
|
+
line('CODE UNDER TEST:');
|
|
87
|
+
line('- alexa-cookie.js: generateAlexaCookie option initialization');
|
|
88
|
+
line('- option passed to lib/proxy.js: baseAmazonPageHandle');
|
|
89
|
+
line('');
|
|
90
|
+
line('INPUT:');
|
|
91
|
+
line(`DE baseAmazonPage: ${de.input.baseAmazonPage}`);
|
|
92
|
+
line(`DE amazonPage: ${de.input.amazonPage}`);
|
|
93
|
+
line(`UK baseAmazonPage: ${uk.input.baseAmazonPage}`);
|
|
94
|
+
line(`UK amazonPage: ${uk.input.amazonPage}`);
|
|
95
|
+
line(`COM baseAmazonPage: ${com.input.baseAmazonPage}`);
|
|
96
|
+
line(`COM amazonPage: ${com.input.amazonPage}`);
|
|
97
|
+
line('');
|
|
98
|
+
line('OBSERVED:');
|
|
99
|
+
line(`DE baseAmazonPageHandle: ${de.output.baseAmazonPageHandle}`);
|
|
100
|
+
line(`UK baseAmazonPageHandle: ${uk.output.baseAmazonPageHandle}`);
|
|
101
|
+
line(`COM baseAmazonPageHandle: ${com.output.baseAmazonPageHandle}`);
|
|
102
|
+
line('');
|
|
103
|
+
line('ASSERTIONS:');
|
|
104
|
+
recordAssertion('DE baseAmazonPageHandle === "_de"', () => {
|
|
105
|
+
assert.strictEqual(de.output.baseAmazonPageHandle, '_de');
|
|
106
|
+
});
|
|
107
|
+
recordAssertion('UK baseAmazonPageHandle === "_uk"', () => {
|
|
108
|
+
assert.strictEqual(uk.output.baseAmazonPageHandle, '_uk');
|
|
109
|
+
});
|
|
110
|
+
recordAssertion('COM baseAmazonPageHandle === ""', () => {
|
|
111
|
+
assert.strictEqual(com.output.baseAmazonPageHandle, '');
|
|
112
|
+
});
|
|
113
|
+
line('');
|
|
114
|
+
line('RESULT: PASS');
|
|
115
|
+
writeOutput();
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (!lines.includes('RESULT: PASS')) {
|
|
118
|
+
line('');
|
|
119
|
+
line('RESULT: FAIL');
|
|
120
|
+
writeOutput();
|
|
121
|
+
}
|
|
122
|
+
throw err;
|
|
123
|
+
}
|