-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathbackground.js
More file actions
470 lines (416 loc) Β· 20 KB
/
background.js
File metadata and controls
470 lines (416 loc) Β· 20 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
import { NETWORK_CONFIG } from './config.js';
let tabId = null;
let networkData = [];
let requestsMap = new Map(); // To match requests with responses
let analysisCallback = null;
let analysisTimer = null;
let domContent = null; // Store captured DOM content
let networkUrlsFilter = []; // Store network URLs to filter by
console.log('Background script loaded');
// Handle extension icon click - open options page
chrome.action.onClicked.addListener(() => {
chrome.runtime.openOptionsPage();
});
// Centralized function to stop analysis and clean up
const stopAnalysis = () => {
console.log('π Stopping analysis...');
if (analysisTimer) {
clearTimeout(analysisTimer);
analysisTimer = null;
console.log(' β
Timer cleared');
}
if (tabId) {
console.log(` π Detaching debugger from tab ${tabId}`);
chrome.debugger.detach({ tabId: tabId }).catch((error) => {
console.log(' β οΈ Error detaching debugger (tab might be gone):', error);
});
if (analysisCallback) {
// Convert map to array for sending
const finalData = Array.from(requestsMap.values());
console.log(` π Sending results: ${finalData.length} requests captured`);
finalData.forEach((req, i) => {
console.log(` Request ${i+1}: ${req.method} ${req.url} (Status: ${req.status || 'pending'})`);
});
// Include DOM content in results if available
const results = {
data: finalData,
domContent: domContent
};
if (domContent) {
console.log(` π Including DOM content: ${domContent.textContent.length} chars of text from ${domContent.url}`);
}
analysisCallback(results);
if (chrome.runtime.lastError) {
console.log(" β οΈ Popup closed before sending results:", chrome.runtime.lastError);
}
}
// Close the analysis tab
console.log(` ποΈ Closing analysis tab ${tabId}`);
chrome.tabs.remove(tabId).catch((error) => {
console.log(' β οΈ Error closing tab:', error);
});
}
tabId = null;
networkData = [];
requestsMap.clear();
analysisCallback = null;
domContent = null; // Reset DOM content
networkUrlsFilter = []; // Reset network URLs filter
console.log(' β
Cleanup complete');
};
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log('π¨ Received message:', request);
if (request.type === 'startAnalysis') {
// If an analysis is already running, stop it first.
if (tabId) {
console.log('π Analysis already running, stopping previous analysis');
stopAnalysis();
}
const { url, networkUrls } = request;
networkUrlsFilter = networkUrls || []; // Store network URLs filter
console.log(`π Starting analysis for: ${url}`);
if (networkUrlsFilter.length > 0) {
console.log(`π‘ Filtering by network URLs: ${networkUrlsFilter.join(', ')}`);
}
networkData = [];
requestsMap.clear();
domContent = null; // Reset DOM content
analysisCallback = sendResponse;
// Create a blank tab first, then attach debugger, then navigate
chrome.tabs.create({ url: 'about:blank' }, (newTab) => {
console.log(`π Created blank tab: ${newTab.id}, will navigate to ${url}`);
tabId = newTab.id;
chrome.debugger.attach({ tabId: tabId }, NETWORK_CONFIG.DEBUGGER_VERSION, () => {
if (chrome.runtime.lastError) {
console.error('β Failed to attach debugger:', chrome.runtime.lastError);
if (analysisCallback) {
analysisCallback({ error: chrome.runtime.lastError.message });
}
tabId = null;
analysisCallback = null;
return;
}
console.log('π§ Debugger attached successfully');
// Enable Network domain with additional events for complete header capture
chrome.debugger.sendCommand({ tabId: tabId }, 'Network.enable', {}, () => {
if (chrome.runtime.lastError) {
console.error('β Failed to enable Network domain:', chrome.runtime.lastError);
if (analysisCallback) {
analysisCallback({ error: chrome.runtime.lastError.message });
}
return;
} else {
console.log('π Network domain enabled');
// Enable extra info events to capture complete headers including cookies
chrome.debugger.sendCommand({ tabId: tabId }, 'Network.enableReportingApi', { enable: true }, () => {
if (chrome.runtime.lastError) {
console.log('β οΈ Could not enable reporting API (might not be available):', chrome.runtime.lastError.message);
}
});
// Now navigate to the actual URL - this ensures we capture the main document request
console.log(`π§ Navigating to: ${url}`);
chrome.tabs.update(tabId, { url: url }, () => {
if (chrome.runtime.lastError) {
console.error('β Failed to navigate to URL:', chrome.runtime.lastError);
if (analysisCallback) {
analysisCallback({ error: chrome.runtime.lastError.message });
}
return;
}
console.log('π Navigation started successfully');
});
}
});
});
});
return true; // Response will be sent asynchronously
}
else if (request.type === 'domContent') {
console.log('π Received DOM content from injected script');
domContent = request.data;
console.log(` π Captured ${domContent.html.length} chars of HTML and ${domContent.textContent.length} chars of text`);
// If we have requests that failed to get response bodies, try to match them with DOM content
if (domContent.url.includes('paid_memberships') || domContent.url.includes('youtube.com')) {
requestsMap.forEach((requestData, requestId) => {
if (requestData.responseBody && requestData.responseBody.includes('Response body not available')) {
console.log(` π Adding DOM content as fallback for failed request: ${requestData.url}`);
requestData.responseBody = `(DOM Content Fallback)\n\nPage Title: ${domContent.title}\n\nPage Text Content:\n${domContent.textContent}\n\n--- Full HTML ---\n${domContent.html}`;
}
});
}
}
return true; // Response will be sent asynchronously
});
chrome.tabs.onUpdated.addListener((updatedTabId, changeInfo) => {
if (updatedTabId === tabId && changeInfo.status === 'complete') {
console.log(`π Page loaded completely in tab ${tabId}, starting ${NETWORK_CONFIG.ANALYSIS_TIMEOUT_MS/1000}-second timer`);
// Inject content script to capture DOM content as fallback
console.log(`π Injecting content script to capture DOM content`);
chrome.scripting.executeScript({
target: { tabId: tabId },
function: () => {
// Wait a bit for dynamic content to load
setTimeout(() => {
const pageData = {
url: window.location.href,
title: document.title,
html: document.documentElement.outerHTML,
textContent: document.body ? document.body.innerText : '',
timestamp: Date.now()
};
// Send page data back to background script
chrome.runtime.sendMessage({
type: 'domContent',
data: pageData
});
}, 2000); // Wait 2 seconds for dynamic content
}
}).catch((error) => {
console.log(`β οΈ Could not inject content script: ${error}`);
});
// Page has loaded. Wait for a few seconds for async requests to finish.
if (analysisTimer) clearTimeout(analysisTimer);
analysisTimer = setTimeout(() => {
console.log(`β° ${NETWORK_CONFIG.ANALYSIS_TIMEOUT_MS/1000}-second timer expired, stopping analysis`);
stopAnalysis();
}, NETWORK_CONFIG.ANALYSIS_TIMEOUT_MS);
}
});
chrome.debugger.onEvent.addListener((source, method, params) => {
if (source.tabId && source.tabId === tabId) {
// Debug: Log all Network events we receive
if (method.startsWith('Network.')) {
console.log(`π§ Network event: ${method}`);
}
if (method === 'Network.requestWillBeSent') {
console.log(`π€ Request: ${params.request.method} ${params.request.url}`);
// If network URLs filter is active, check if this request matches any of the filtered URLs
if (networkUrlsFilter.length > 0) {
const requestUrl = params.request.url;
const matchesFilter = networkUrlsFilter.some(filterUrl => {
// Check if the request URL contains the filter URL
return requestUrl.includes(filterUrl) || filterUrl.includes(requestUrl);
});
if (!matchesFilter) {
console.log(`βοΈ Skipping request (doesn't match filter): ${requestUrl}`);
return; // Skip this request
}
console.log(`β
Request matches filter: ${requestUrl}`);
}
// Capture request details
const requestData = {
requestId: params.requestId,
url: params.request.url,
method: params.request.method,
requestHeaders: params.request.headers,
requestBody: params.request.postData || null,
timestamp: params.timestamp,
responseReceived: false,
responseHeaders: null,
responseBody: null,
status: null,
mimeType: null
};
requestsMap.set(params.requestId, requestData);
}
// Handle extra request info with complete headers including cookies
else if (method === 'Network.requestWillBeSentExtraInfo') {
console.log(`π Extra request info for: ${params.requestId}`);
const requestData = requestsMap.get(params.requestId);
if (requestData && params.headers) {
// Merge the extra headers with existing headers
console.log(` π Updating headers with complete info (${Object.keys(params.headers).length} headers)`);
requestData.requestHeaders = { ...requestData.requestHeaders, ...params.headers };
// Log some of the extra headers we got
const extraHeaderKeys = Object.keys(params.headers);
if (extraHeaderKeys.includes('cookie')) {
console.log(` πͺ Cookie header captured`);
}
if (extraHeaderKeys.includes('priority')) {
console.log(` β‘ Priority header captured`);
}
}
}
else if (method === 'Network.responseReceived') {
console.log(`π₯ Response: ${params.response.status} for ${params.response.url}`);
let requestData = requestsMap.get(params.requestId);
// If we don't have the request data (main document request might be missed), create it if it matches filter
if (!requestData) {
// Check if this response matches our filter
if (networkUrlsFilter.length > 0) {
const responseUrl = params.response.url;
const matchesFilter = networkUrlsFilter.some(filterUrl => {
return responseUrl.includes(filterUrl) || filterUrl.includes(responseUrl);
});
if (matchesFilter) {
console.log(`β
Creating missing request data for response that matches filter: ${responseUrl}`);
// Create request data for the missed request
requestData = {
requestId: params.requestId,
url: params.response.url,
method: 'GET', // Assume GET for main document
requestHeaders: {},
requestBody: null,
timestamp: params.timestamp,
responseReceived: false,
responseHeaders: null,
responseBody: null,
status: null,
mimeType: null
};
requestsMap.set(params.requestId, requestData);
} else {
console.log(`βοΈ Skipping response (doesn't match filter): ${responseUrl}`);
return;
}
} else {
// No filter, but we still missed the request - create basic entry
console.log(`β οΈ Creating missing request data for response: ${params.response.url}`);
requestData = {
requestId: params.requestId,
url: params.response.url,
method: 'GET',
requestHeaders: {},
requestBody: null,
timestamp: params.timestamp,
responseReceived: false,
responseHeaders: null,
responseBody: null,
status: null,
mimeType: null
};
requestsMap.set(params.requestId, requestData);
}
}
if (requestData) {
// Update with response details
requestData.responseReceived = true;
requestData.status = params.response.status;
requestData.statusText = params.response.statusText;
requestData.responseHeaders = params.response.headers;
requestData.mimeType = params.response.mimeType;
const contentLength = params.response.headers['Content-Length'] || params.response.headers['content-length'];
// Don't try to get body for data URIs, redirects, empty responses, etc.
if (params.response.url.startsWith('data:') ||
params.response.status === 204 ||
(params.response.status >= 300 && params.response.status < 400) ||
contentLength === '0') {
requestData.responseBody = `(No content: status ${params.response.status})`;
console.log(` βοΈ Skipping body for ${params.response.url} (status ${params.response.status})`);
return;
}
// Get response body immediately - don't wait
console.log(` π Getting response body for ${params.response.url}`);
requestData.responseBody = '(Loading...)'; // Set temporary status
// Try to get response body with retry logic
const attemptGetResponseBody = (attempt = 1, maxAttempts = 3) => {
chrome.debugger.sendCommand({ tabId: source.tabId }, 'Network.getResponseBody', { requestId: params.requestId }, (response) => {
if (chrome.runtime.lastError) {
const errorMsg = chrome.runtime.lastError.message;
console.log(` β οΈ Attempt ${attempt} failed to get response body: ${errorMsg}`);
// If it's a JSON error, try to parse it for more info
let parsedError = errorMsg;
try {
if (errorMsg.includes('{') && errorMsg.includes('}')) {
const jsonMatch = errorMsg.match(/\{.*\}/);
if (jsonMatch) {
const errorObj = JSON.parse(jsonMatch[0]);
parsedError = errorObj.message || errorMsg;
}
}
} catch (e) {
// Keep original error message if parsing fails
}
if (attempt < maxAttempts && !errorMsg.includes('No data found')) {
// Retry for certain types of errors, but not for "No data found"
console.log(` π Retrying in 100ms (attempt ${attempt + 1}/${maxAttempts})`);
setTimeout(() => attemptGetResponseBody(attempt + 1, maxAttempts), 100);
return;
}
// Final failure - set error message
if (errorMsg.includes('No data found')) {
requestData.responseBody = `(Response body not available - likely streamed or service worker handled)`;
} else {
requestData.responseBody = `(Could not get response body: ${parsedError})`;
}
return;
}
if (response) {
if (response.base64Encoded) {
// For base64 content, try to decode if it's text
try {
const decoded = atob(response.body);
// Check if it looks like text (common for HTML/JSON/XML)
if (decoded.length < 1000000 && /^[\x09\x0A\x0D\x20-\x7E\u00A0-\uFFFF]*$/.test(decoded.substring(0, 1000))) {
requestData.responseBody = decoded;
console.log(` π Decoded base64 content captured (${decoded.length} chars)`);
} else {
requestData.responseBody = '[Binary content - base64 encoded]';
console.log(` π¦ Binary content captured`);
}
} catch (e) {
requestData.responseBody = '[Binary content - base64 encoded]';
console.log(` π¦ Could not decode base64 content`);
}
} else {
requestData.responseBody = response.body || '(empty body)';
console.log(` π Text content captured (${response.body ? response.body.length : 0} chars)`);
}
} else {
requestData.responseBody = '(no response body data)';
console.log(` β No response body available`);
}
});
};
// Start the attempt
attemptGetResponseBody();
}
}
// Handle extra response info with complete headers
else if (method === 'Network.responseReceivedExtraInfo') {
console.log(`π Extra response info for: ${params.requestId}`);
const requestData = requestsMap.get(params.requestId);
if (requestData && params.headers) {
// Merge the extra response headers with existing headers
console.log(` π Updating response headers with complete info (${Object.keys(params.headers).length} headers)`);
requestData.responseHeaders = { ...requestData.responseHeaders, ...params.headers };
}
}
// Also handle loading finished event for additional capture opportunities
else if (method === 'Network.loadingFinished') {
const requestData = requestsMap.get(params.requestId);
if (requestData && requestData.responseBody === '(Loading...)') {
console.log(` π Loading finished for ${requestData.url}, attempting final body capture`);
// Try one more time to get the response body
chrome.debugger.sendCommand({ tabId: source.tabId }, 'Network.getResponseBody', { requestId: params.requestId }, (response) => {
if (!chrome.runtime.lastError && response) {
if (response.base64Encoded) {
try {
const decoded = atob(response.body);
if (decoded.length < 1000000 && /^[\x09\x0A\x0D\x20-\x7E\u00A0-\uFFFF]*$/.test(decoded.substring(0, 1000))) {
requestData.responseBody = decoded;
console.log(` π Final capture: Decoded base64 content (${decoded.length} chars)`);
} else {
requestData.responseBody = '[Binary content - base64 encoded]';
}
} catch (e) {
requestData.responseBody = '[Binary content - base64 encoded]';
}
} else {
requestData.responseBody = response.body || '(empty body)';
console.log(` π Final capture: Text content (${response.body ? response.body.length : 0} chars)`);
}
} else if (requestData.responseBody === '(Loading...)') {
requestData.responseBody = '(Response body not available - likely streamed or handled by service worker)';
}
});
}
}
}
});
chrome.tabs.onRemoved.addListener((removedTabId) => {
if (removedTabId === tabId) {
console.log(`ποΈ Analysis tab ${removedTabId} was closed manually`);
stopAnalysis();
}
});