-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
396 lines (339 loc) · 13.7 KB
/
Copy pathbackground.js
File metadata and controls
396 lines (339 loc) · 13.7 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
// Create context menu when extension is installed
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: "sendToQbittorrent",
title: "Send to qBittorrent",
contexts: ["link"]
});
});
// Handle context menu clicks
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId === "sendToQbittorrent") {
const url = info.linkUrl;
await sendToQbittorrent(url, tab.id);
}
});
// Function to send download to qBittorrent
async function sendToQbittorrent(downloadUrl, tabId) {
try {
// Get stored settings
const settings = await chrome.storage.sync.get(['qbUrl', 'qbUsername', 'qbPassword', 'torrentSites']);
if (!settings.qbUrl || !settings.qbUsername || !settings.qbPassword) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon48.png',
title: 'qBittorrent Settings Missing',
message: 'Please configure qBittorrent settings in the extension options.'
});
return;
}
// Check if this URL needs authentication
const torrentFileUrl = await getAuthenticatedTorrentUrl(downloadUrl, settings.torrentSites, tabId);
// Remove trailing slash from URL if present
const baseUrl = settings.qbUrl.replace(/\/$/, '');
// Step 1: Login to qBittorrent and get the SID cookie
const loginResponse = await fetch(`${baseUrl}/api/v2/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': baseUrl
},
body: new URLSearchParams({
username: settings.qbUsername,
password: settings.qbPassword
}),
credentials: 'include',
mode: 'cors'
});
if (!loginResponse.ok) {
const text = await loginResponse.text();
throw new Error(`Login failed: ${loginResponse.status} - ${text}`);
}
const loginText = await loginResponse.text();
// Accept both "Ok." and empty response (HTTP 204)
if (loginText !== 'Ok.' && loginText.trim() !== '' && loginResponse.status !== 204) {
throw new Error(`Invalid credentials: ${loginText}`);
}
console.log('Login successful, adding torrent:', torrentFileUrl);
// Extract SID cookie from response
const cookies = await chrome.cookies.getAll({ url: baseUrl });
console.log('Available cookies:', cookies);
const sidCookie = cookies.find(c => c.name === 'SID');
if (!sidCookie) {
console.warn('No SID cookie found, trying anyway...');
} else {
console.log('SID cookie found:', sidCookie.value);
}
// Small delay to ensure session is established
await new Promise(resolve => setTimeout(resolve, 100));
// Step 2: Add the torrent/download with explicit cookie
const addHeaders = {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': baseUrl
};
// If we have a SID cookie, add it to the headers
if (sidCookie) {
addHeaders['Cookie'] = `SID=${sidCookie.value}`;
}
const addResponse = await fetch(`${baseUrl}/api/v2/torrents/add`, {
method: 'POST',
headers: addHeaders,
body: new URLSearchParams({
urls: torrentFileUrl
}),
credentials: 'include',
mode: 'cors'
});
console.log('Add torrent response status:', addResponse.status);
const responseText = await addResponse.text();
console.log('Add torrent response text:', responseText);
if (addResponse.ok) {
// Status 202 means accepted/pending
if (addResponse.status === 202) {
try {
const jsonResponse = JSON.parse(responseText);
const totalAdded = (jsonResponse.success_count || 0) + (jsonResponse.pending_count || 0);
if (totalAdded > 0) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon48.png',
title: 'Success!',
message: `Torrent added to qBittorrent!`
});
return;
}
} catch (e) {
// Not JSON, fall through
}
}
// Check the response - qBittorrent returns "Ok." on success or HTTP 204 (no content)
if (responseText === 'Ok.' || responseText === '' || addResponse.status === 200 || addResponse.status === 204) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon48.png',
title: 'Success!',
message: `Torrent added to qBittorrent!`
});
} else {
throw new Error(`Unexpected response: ${responseText}`);
}
} else {
throw new Error(`Failed to add download: ${addResponse.status} - ${responseText}`);
}
} catch (error) {
console.error('Error:', error);
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon48.png',
title: 'Error',
message: `Failed to send to qBittorrent: ${error.message}`
});
}
}
// Function to get authenticated torrent URL
async function getAuthenticatedTorrentUrl(downloadUrl, torrentSites, tabId) {
if (!torrentSites || torrentSites.length === 0) {
return downloadUrl; // No sites configured, use URL as-is
}
try {
// Extract domain from download URL
const urlObj = new URL(downloadUrl);
const domain = urlObj.hostname.toLowerCase();
console.log('Looking for site config for domain:', domain);
// Find matching site configuration (case-insensitive)
const siteConfig = torrentSites.find(site =>
domain.includes(site.domain.toLowerCase()) ||
site.domain.toLowerCase().includes(domain)
);
if (!siteConfig) {
console.log('No matching site config found for', domain);
return downloadUrl; // No config for this domain
}
console.log('Found site config for', siteConfig.domain);
// If cookies are provided, try to use them directly first
if (siteConfig.cookies) {
console.log('Using stored cookies for', siteConfig.domain);
// Parse the cookies to get individual cookie objects
const cookiePairs = siteConfig.cookies.split(';').map(c => c.trim());
const cookieString = cookiePairs.join('; ');
console.log('Cookie string:', cookieString);
// Try to fetch directly with cookies first
try {
const torrentResponse = await fetch(downloadUrl, {
method: 'GET',
headers: {
'Cookie': cookieString,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'application/x-bittorrent,*/*',
'Referer': `https://${siteConfig.domain}/`,
'Accept-Language': 'en-US,en;q=0.9'
},
credentials: 'omit',
redirect: 'follow'
});
console.log('Torrent fetch response status:', torrentResponse.status);
console.log('Torrent fetch response type:', torrentResponse.headers.get('content-type'));
if (torrentResponse.ok) {
// Check if we got HTML (login page) instead of a torrent file
const contentType = torrentResponse.headers.get('content-type') || '';
if (contentType.includes('text/html')) {
console.warn('Got HTML instead of torrent file with direct cookie fetch');
// Fall through to try content script approach
} else {
console.log('Successfully fetched torrent file with cookies');
const torrentBlob = await torrentResponse.blob();
const base64 = await blobToBase64(torrentBlob);
return `data:application/x-bittorrent;base64,${base64}`;
}
}
} catch (error) {
console.warn('Direct cookie fetch failed:', error.message);
}
// If direct fetch failed, try content script approach (for HttpOnly cookies)
console.log('Attempting to fetch via content script (HttpOnly cookie support)');
try {
// First, ensure content script is injected
await chrome.scripting.executeScript({
target: { tabId: tabId },
files: ['content.js']
}).catch(() => {
// Content script might already be injected, that's okay
});
// Small delay to ensure content script is ready
await new Promise(resolve => setTimeout(resolve, 100));
// Try to use the content script in the current tab
const response = await chrome.tabs.sendMessage(tabId, {
action: 'fetchWithCookies',
url: downloadUrl
});
if (response && response.success) {
console.log('Successfully fetched torrent file via content script');
return `data:application/x-bittorrent;base64,${response.base64}`;
} else {
console.warn('Content script fetch failed:', response?.error);
}
} catch (error) {
console.warn('Content script not available or fetch failed:', error.message);
}
}
// Fall back to username/password login if no cookies or content script failed
if (siteConfig.username && siteConfig.password) {
console.log('Using username/password login for', siteConfig.domain);
const loginUrl = siteConfig.loginUrl || `https://${siteConfig.domain}/login.php`;
console.log('Logging in to', loginUrl);
// Step 1: Login to the torrent site
const loginResponse = await fetch(loginUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
username: siteConfig.username,
password: siteConfig.password
}),
credentials: 'include',
redirect: 'follow'
});
if (!loginResponse.ok) {
console.warn('Login to torrent site failed:', loginResponse.status);
return downloadUrl; // Fall back to original URL
}
console.log('Torrent site login successful');
// Step 2: Fetch the torrent file with authenticated session
const torrentResponse = await fetch(downloadUrl, {
credentials: 'include'
});
if (!torrentResponse.ok) {
console.warn('Failed to fetch torrent file:', torrentResponse.status);
return downloadUrl;
}
// Get the torrent file as a blob
const torrentBlob = await torrentResponse.blob();
// Convert blob to base64
const base64 = await blobToBase64(torrentBlob);
// Return as data URL that qBittorrent can use
return `data:application/x-bittorrent;base64,${base64}`;
}
// No authentication method available
console.warn('No valid authentication method for', siteConfig.domain);
return downloadUrl;
} catch (error) {
console.error('Error authenticating torrent site:', error);
return downloadUrl; // Fall back to original URL
}
}
// Helper function to convert blob to base64
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
// Listen for messages from popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'testConnection') {
testConnection(request.settings).then(result => {
sendResponse(result);
});
return true; // Keep channel open for async response
}
});
// Test connection function
async function testConnection(settings) {
try {
const baseUrl = settings.qbUrl.replace(/\/$/, '');
console.log('Testing connection to:', baseUrl);
const response = await fetch(`${baseUrl}/api/v2/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': baseUrl
},
body: new URLSearchParams({
username: settings.qbUsername,
password: settings.qbPassword
}),
credentials: 'include',
mode: 'cors'
});
console.log('Response status:', response.status);
console.log('Response type:', response.type);
console.log('Response headers:', [...response.headers.entries()]);
if (response.ok) {
const text = await response.text();
console.log('Response text:', text);
console.log('Response text length:', text.length);
console.log('Response trimmed:', text.trim());
// HTTP 204 No Content = successful login (newer qBittorrent versions)
if (response.status === 204 || text === 'Ok.' || text.trim() === '') {
return { success: true, message: 'Connection successful! ✓' };
} else if (text === 'Fails.') {
return { success: false, message: 'Invalid username or password' };
} else {
return { success: false, message: `Unexpected response: "${text}" (length: ${text.length})` };
}
} else if (response.status === 401) {
return { success: false, message: 'Authentication failed - check username and password' };
} else if (response.status === 0 || response.type === 'opaque') {
return { success: false, message: 'CORS error - see troubleshooting in README' };
} else {
const text = await response.text();
console.log('Error response body:', text);
return { success: false, message: `Connection failed: HTTP ${response.status} - ${text.substring(0, 100)}` };
}
} catch (error) {
console.error('Test connection error:', error);
if (error.message.includes('Failed to fetch')) {
return {
success: false,
message: 'Cannot reach qBittorrent. Check URL and ensure Web UI is enabled. CORS may need configuration.'
};
}
return { success: false, message: `Error: ${error.message}` };
}
}