alexa-cookie2 5.0.2 → 5.0.4

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.
@@ -0,0 +1,261 @@
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const vm = require('vm');
5
+ const EventEmitter = require('events');
6
+
7
+ const testName = 'registration-hardening';
8
+ const outputDir = process.env.ALEXA_COOKIE_TEST_OUTPUT_DIR || path.join(__dirname, '..', 'test-output');
9
+ const outputFile = path.join(outputDir, `${testName}.txt`);
10
+ const lines = [];
11
+
12
+ function line(value = '') {
13
+ lines.push(value);
14
+ }
15
+
16
+ function writeOutput() {
17
+ fs.mkdirSync(outputDir, { recursive: true });
18
+ fs.writeFileSync(outputFile, `${lines.join('\n')}\n`);
19
+ }
20
+
21
+ function recordAssertion(description, fn) {
22
+ try {
23
+ fn();
24
+ line(`${description}: PASS`);
25
+ } catch (err) {
26
+ line(`${description}: FAIL`);
27
+ writeOutput();
28
+ throw err;
29
+ }
30
+ }
31
+
32
+ function parseCookies(cookieHeader) {
33
+ const result = {};
34
+ for (const part of String(cookieHeader || '').split(';')) {
35
+ const trimmed = part.trim();
36
+ if (!trimmed) continue;
37
+ const idx = trimmed.indexOf('=');
38
+ if (idx === -1) continue;
39
+ result[trimmed.slice(0, idx)] = trimmed.slice(idx + 1);
40
+ }
41
+ return result;
42
+ }
43
+
44
+ const cookieFile = path.join(__dirname, '..', 'alexa-cookie.js');
45
+ const source = fs.readFileSync(cookieFile, 'utf8');
46
+
47
+ function createProxyStub(proxyData) {
48
+ return {
49
+ initAmazonProxy(_options, callbackCookie) {
50
+ callbackCookie(null, Object.assign({
51
+ loginCookie: 'connect.sid=LOCAL_BROWSER; session-id=SID_BROWSER; ubid-acbde=UBID_BROWSER; frc=FRC_PROXY; map-md=MAPMD_PROXY',
52
+ authorization_code: 'AUTH_CODE_FROM_PROXY',
53
+ verifier: 'VERIFIER_FROM_PROXY',
54
+ deviceId: 'DEVICE_ID_FROM_PROXY',
55
+ frc: 'FRC_PROXY',
56
+ 'map-md': 'MAPMD_PROXY'
57
+ }, proxyData || {}));
58
+ }
59
+ };
60
+ }
61
+
62
+ function loadCookieModule(overrides = {}) {
63
+ const module = { exports: {} };
64
+ const sandbox = {
65
+ Buffer,
66
+ URL,
67
+ __dirname: path.dirname(cookieFile),
68
+ console,
69
+ module,
70
+ exports: module.exports,
71
+ require(name) {
72
+ if (name === 'https') return overrides.https;
73
+ if (name === './lib/proxy.js') return createProxyStub(overrides.proxyData);
74
+ if (name === 'cookie') return { parse: parseCookies };
75
+ return require(name);
76
+ }
77
+ };
78
+ vm.runInNewContext(source, sandbox, { filename: cookieFile });
79
+ return module.exports;
80
+ }
81
+
82
+ function createFakeHttps(config, calls) {
83
+ function responseFor(options) {
84
+ if (options.path === '/auth/register') {
85
+ return {
86
+ statusCode: 200,
87
+ headers: { 'set-cookie': ['session-id=SID_REGISTER; Path=/; Domain=.amazon.de'] },
88
+ body: JSON.stringify({
89
+ response: {
90
+ success: {
91
+ tokens: {
92
+ bearer: {
93
+ refresh_token: 'REFRESH_TOKEN_AFTER_REGISTER',
94
+ access_token: 'ACCESS_TOKEN_AFTER_REGISTER'
95
+ },
96
+ mac_dms: {
97
+ device_private_key: 'PRIVATE_KEY_AFTER_REGISTER',
98
+ adp_token: 'ADP_TOKEN_AFTER_REGISTER'
99
+ },
100
+ website_cookies: [
101
+ { Name: 'session-token', Value: 'SESSION_TOKEN_AFTER_REGISTER' },
102
+ { Name: 'at-acbde', Value: 'AT_AFTER_REGISTER' },
103
+ { Name: 'sess-at-acbde', Value: 'SESS_AT_AFTER_REGISTER' }
104
+ ]
105
+ }
106
+ }
107
+ }
108
+ })
109
+ };
110
+ }
111
+ if (options.host === 'api.amazonalexa.com' && options.path === '/v1/devices/@self/capabilities') {
112
+ return { statusCode: 204, headers: {}, body: '' };
113
+ }
114
+ if (options.path === '/api/users/me?platform=ios&version=2.2.651540.0') {
115
+ if (config.userDataMode === 'empty401') {
116
+ return { statusCode: 401, headers: {}, body: '' };
117
+ }
118
+ return {
119
+ statusCode: 200,
120
+ headers: {},
121
+ body: JSON.stringify({ marketPlaceDomainName: 'www.amazon.de' })
122
+ };
123
+ }
124
+ if (options.path === '/ap/exchangetoken/cookies') {
125
+ if (config.localCookieMode === 'error') {
126
+ return { statusCode: 200, headers: {}, body: '{}' };
127
+ }
128
+ return {
129
+ statusCode: 200,
130
+ headers: { 'set-cookie': ['session-id=SID_LOCAL_HEADER; Path=/; Domain=.amazon.de'] },
131
+ body: JSON.stringify({
132
+ response: {
133
+ tokens: {
134
+ cookies: {
135
+ '.amazon.de': [
136
+ { Name: 'session-id', Value: 'SID_LOCAL' },
137
+ { Name: 'ubid-acbde', Value: 'UBID_LOCAL' }
138
+ ]
139
+ }
140
+ }
141
+ }
142
+ })
143
+ };
144
+ }
145
+ if (options.path === '/api/language') {
146
+ return {
147
+ statusCode: 200,
148
+ headers: { 'set-cookie': ['csrf=CSRF_FROM_API; Path=/; Domain=.amazon.de'] },
149
+ body: '{}'
150
+ };
151
+ }
152
+ throw new Error(`Unexpected request: ${options.method || 'GET'} ${options.host}${options.path}`);
153
+ }
154
+
155
+ return {
156
+ request(options, callback) {
157
+ const req = new EventEmitter();
158
+ let requestBody = '';
159
+ req.write = (chunk) => {
160
+ requestBody += chunk;
161
+ };
162
+ req.end = () => {
163
+ const call = {
164
+ method: options.method || 'GET',
165
+ host: options.host,
166
+ path: options.path,
167
+ headers: Object.assign({}, options.headers),
168
+ requestBody,
169
+ status: null
170
+ };
171
+ calls.push(call);
172
+ const response = responseFor(options);
173
+ call.status = response.statusCode;
174
+ const res = new EventEmitter();
175
+ res.statusCode = response.statusCode;
176
+ res.headers = response.headers || {};
177
+ res.socket = { end() {} };
178
+ callback(res);
179
+ if (response.body) res.emit('data', Buffer.from(response.body));
180
+ res.emit('end');
181
+ };
182
+ return req;
183
+ }
184
+ };
185
+ }
186
+
187
+ function runScenario(config = {}) {
188
+ const calls = [];
189
+ const callbacks = [];
190
+ const cookieModule = loadCookieModule({
191
+ https: createFakeHttps(config, calls),
192
+ proxyData: config.proxyData
193
+ });
194
+
195
+ cookieModule.generateAlexaCookie({
196
+ proxyOwnIp: '127.0.0.1',
197
+ proxyPort: 3456,
198
+ proxyListenBind: '0.0.0.0',
199
+ baseAmazonPage: 'amazon.de',
200
+ amazonPage: 'amazon.de',
201
+ acceptLanguage: 'de-DE',
202
+ proxyLogLevel: 'silent',
203
+ logger: () => {}
204
+ }, (err, data) => {
205
+ callbacks.push({ err, data });
206
+ });
207
+
208
+ return { calls, callbacks };
209
+ }
210
+
211
+ try {
212
+ line('TEST: registration hardening');
213
+ line('');
214
+ line('CODE UNDER TEST:');
215
+ line('- alexa-cookie.js: handleTokenRegistration()');
216
+ line('- alexa-cookie.js: optional /api/users/me handling');
217
+ line('- alexa-cookie.js: getLocalCookies() error path after registration');
218
+ line('');
219
+
220
+ const sanitizedScenario = runScenario();
221
+ const registerCall = sanitizedScenario.calls.find((call) => call.path === '/auth/register');
222
+ const registerBody = JSON.parse(registerCall.requestBody);
223
+
224
+ line('ASSERTIONS:');
225
+ recordAssertion('registration Cookie header removes unrelated local browser cookie', () => {
226
+ assert.ok(!registerCall.headers.Cookie.includes('connect.sid=LOCAL_BROWSER'));
227
+ });
228
+ recordAssertion('registration body removes unrelated local browser cookie', () => {
229
+ assert.ok(!registerBody.cookies.website_cookies.some((cookie) => cookie.Name === 'connect.sid'));
230
+ });
231
+ recordAssertion('registration body keeps Amazon session cookie', () => {
232
+ assert.ok(registerBody.cookies.website_cookies.some((cookie) => cookie.Name === 'session-id' && cookie.Value === 'SID_BROWSER'));
233
+ });
234
+
235
+ const emptyUserDataScenario = runScenario({ userDataMode: 'empty401' });
236
+ recordAssertion('empty optional user-data response continues when amazonPage is known', () => {
237
+ assert.strictEqual(emptyUserDataScenario.callbacks.length, 1);
238
+ assert.ifError(emptyUserDataScenario.callbacks[0].err);
239
+ assert.strictEqual(emptyUserDataScenario.callbacks[0].data.amazonPage, 'amazon.de');
240
+ assert.ok(emptyUserDataScenario.callbacks[0].data.localCookie.includes('session-id=SID_LOCAL'));
241
+ });
242
+
243
+ const localCookieErrorScenario = runScenario({ localCookieMode: 'error' });
244
+ recordAssertion('local-cookie retrieval error stops registration callback flow', () => {
245
+ assert.strictEqual(localCookieErrorScenario.callbacks.length, 1);
246
+ assert.ok(localCookieErrorScenario.callbacks[0].err);
247
+ assert.match(localCookieErrorScenario.callbacks[0].err.message, /No cookies in Exchange response/);
248
+ assert.strictEqual(localCookieErrorScenario.calls.filter((call) => call.path === '/api/language').length, 0);
249
+ });
250
+
251
+ line('');
252
+ line('RESULT: PASS');
253
+ writeOutput();
254
+ } catch (err) {
255
+ if (!lines.includes('RESULT: PASS')) {
256
+ line('');
257
+ line('RESULT: FAIL');
258
+ writeOutput();
259
+ }
260
+ throw err;
261
+ }