n8n-nodes-nvk-browser 1.0.76 → 1.0.78
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/dist/nodes/ProfileManagement/CreateProfile/CreateProfile.description.js +2 -1
- package/dist/nodes/ProfileManagement/CreateProfile/CreateProfile.node.js +24 -2
- package/dist/utils/BrowserManager.js +32 -2
- package/dist/utils/ExtensionHandler.d.ts +67 -6
- package/dist/utils/ExtensionHandler.js +486 -28
- package/dist/utils/ProfileManager.d.ts +1 -1
- package/dist/utils/ProfileManager.js +2 -1
- package/dist/utils/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -51,7 +51,8 @@ exports.createProfileFields = [
|
|
|
51
51
|
rows: 4,
|
|
52
52
|
},
|
|
53
53
|
default: '',
|
|
54
|
-
|
|
54
|
+
placeholder: 'nmmhkkegccagdldgiimedpiccmgmieda\nhttps://example.com/extension.crx\n./extensions/my-extension',
|
|
55
|
+
description: 'Extensions to install: Chrome Store ID (32 chars), URL (http://...), or local path. One extension per line. Examples:\n- Chrome Store ID: nmmhkkegccagdldgiimedpiccmgmieda\n- URL: https://example.com/extension.crx\n- Local path: ./extensions/my-extension or /absolute/path/to/extension',
|
|
55
56
|
displayOptions: {
|
|
56
57
|
show: {
|
|
57
58
|
resource: ['profile'],
|
|
@@ -95,8 +95,29 @@ class CreateProfile {
|
|
|
95
95
|
const extensionsInput = this.getNodeParameter('extensions', i) || '';
|
|
96
96
|
// Parse extensions
|
|
97
97
|
const extensions = ExtensionHandler_1.ExtensionHandler.parseExtensions(extensionsInput);
|
|
98
|
-
//
|
|
99
|
-
const
|
|
98
|
+
// Validate extensions và lấy resolved paths
|
|
99
|
+
const extensionPaths = [];
|
|
100
|
+
if (extensions.length > 0) {
|
|
101
|
+
console.log(`[CreateProfile] Validating ${extensions.length} extension(s)...`);
|
|
102
|
+
for (const ext of extensions) {
|
|
103
|
+
try {
|
|
104
|
+
const validation = await ExtensionHandler_1.ExtensionHandler.validateExtension(ext, resolvedProfilesDir);
|
|
105
|
+
if (validation.valid && validation.path) {
|
|
106
|
+
extensionPaths.push(validation.path);
|
|
107
|
+
console.log(`[CreateProfile] ✓ Extension validated: ${ext} -> ${validation.path}`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
console.error(`[CreateProfile] ✗ Extension validation failed: ${ext} - ${validation.error || 'Unknown error'}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error(`[CreateProfile] ✗ Error validating extension "${ext}":`, error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
console.log(`[CreateProfile] Successfully validated ${extensionPaths.length}/${extensions.length} extension(s)`);
|
|
118
|
+
}
|
|
119
|
+
// Tạo profile với cả extensionPaths đã resolve
|
|
120
|
+
const profile = profileManager.createProfile(profileName, proxy, note, extensions, extensionPaths);
|
|
100
121
|
// Lưu proxy authentication vào profile nếu có
|
|
101
122
|
// QUAN TRỌNG: Phải gọi SAU khi createProfile và setProfileName để Preferences file đã được tạo
|
|
102
123
|
// Nhưng phải đảm bảo không bị ghi đè
|
|
@@ -180,6 +201,7 @@ class CreateProfile {
|
|
|
180
201
|
proxy: profile.proxy,
|
|
181
202
|
note: profile.note,
|
|
182
203
|
extensions: profile.extensions,
|
|
204
|
+
extensionPaths: profile.extensionPaths,
|
|
183
205
|
createdAt: profile.createdAt,
|
|
184
206
|
},
|
|
185
207
|
},
|
|
@@ -97,8 +97,38 @@ class BrowserManager {
|
|
|
97
97
|
args.push(...ProxyHandler_1.ProxyHandler.getChromeProxyArgs(proxyConfig));
|
|
98
98
|
}
|
|
99
99
|
// Extension configuration
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// Sử dụng extensionPaths đã resolve thay vì validate lại
|
|
101
|
+
if (profile.extensionPaths && profile.extensionPaths.length > 0) {
|
|
102
|
+
// Kiểm tra paths còn tồn tại không
|
|
103
|
+
const validPaths = [];
|
|
104
|
+
for (const extPath of profile.extensionPaths) {
|
|
105
|
+
const manifestPath = path.join(extPath, 'manifest.json');
|
|
106
|
+
if (fs.existsSync(manifestPath)) {
|
|
107
|
+
validPaths.push(extPath);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
console.warn(`[BrowserManager] Extension path no longer valid (missing manifest.json): ${extPath}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (validPaths.length > 0) {
|
|
114
|
+
args.push(`--load-extension=${validPaths.join(',')}`);
|
|
115
|
+
console.log(`[BrowserManager] Loading ${validPaths.length} extension(s) from cached paths`);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
console.warn(`[BrowserManager] No valid extension paths found, skipping extensions`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (profile.extensions && profile.extensions.length > 0) {
|
|
122
|
+
// Fallback: Nếu không có extensionPaths, validate lại (cho backward compatibility)
|
|
123
|
+
console.log(`[BrowserManager] No extensionPaths found, validating extensions on-the-fly (legacy mode)`);
|
|
124
|
+
try {
|
|
125
|
+
const extensionArgs = await ExtensionHandler_1.ExtensionHandler.getExtensionArgs(profile.extensions, this.profilesDir);
|
|
126
|
+
args.push(...extensionArgs);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error(`[BrowserManager] Error loading extensions:`, error);
|
|
130
|
+
// Continue without extensions rather than failing completely
|
|
131
|
+
}
|
|
102
132
|
}
|
|
103
133
|
// Window configuration
|
|
104
134
|
if (windowConfig) {
|
|
@@ -1,9 +1,70 @@
|
|
|
1
|
+
interface ExtensionValidationResult {
|
|
2
|
+
valid: boolean;
|
|
3
|
+
path?: string;
|
|
4
|
+
error?: string;
|
|
5
|
+
type?: 'store-id' | 'url' | 'local-path';
|
|
6
|
+
}
|
|
1
7
|
export declare class ExtensionHandler {
|
|
8
|
+
private static readonly CACHE_DIR_NAME;
|
|
9
|
+
private static readonly DOWNLOAD_TIMEOUT;
|
|
10
|
+
private static readonly MAX_RETRIES;
|
|
11
|
+
/**
|
|
12
|
+
* Parse extensions từ input string (mỗi extension một dòng)
|
|
13
|
+
*/
|
|
2
14
|
static parseExtensions(extensionInput: string): string[];
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Xác định loại extension: Chrome Store ID, URL, hoặc Local Path
|
|
17
|
+
*/
|
|
18
|
+
private static detectExtensionType;
|
|
19
|
+
/**
|
|
20
|
+
* Validate và resolve extension path
|
|
21
|
+
*/
|
|
22
|
+
static validateExtension(extension: string, workspacePath?: string): Promise<ExtensionValidationResult>;
|
|
23
|
+
/**
|
|
24
|
+
* Lấy cache directory path
|
|
25
|
+
*/
|
|
26
|
+
private static getCacheDir;
|
|
27
|
+
/**
|
|
28
|
+
* Đảm bảo cache directory tồn tại
|
|
29
|
+
*/
|
|
30
|
+
private static ensureCacheDir;
|
|
31
|
+
/**
|
|
32
|
+
* Tạo cache key từ extension identifier
|
|
33
|
+
*/
|
|
34
|
+
private static getCacheKey;
|
|
35
|
+
/**
|
|
36
|
+
* Kiểm tra extension đã được cache chưa
|
|
37
|
+
*/
|
|
38
|
+
private static getCachedExtensionPath;
|
|
39
|
+
/**
|
|
40
|
+
* Download file từ URL
|
|
41
|
+
*/
|
|
42
|
+
private static downloadFile;
|
|
43
|
+
/**
|
|
44
|
+
* Extract CRX file
|
|
45
|
+
* CRX format: Header (magic number + version + pub key len + sig len + pub key + sig) + ZIP content
|
|
46
|
+
*/
|
|
47
|
+
private static extractCrxFile;
|
|
48
|
+
/**
|
|
49
|
+
* Extract ZIP buffer to directory
|
|
50
|
+
*/
|
|
51
|
+
private static extractZipBuffer;
|
|
52
|
+
/**
|
|
53
|
+
* Manual ZIP extraction
|
|
54
|
+
* Supports stored (method 0) and deflated (method 8) compression
|
|
55
|
+
*/
|
|
56
|
+
private static extractZipManually;
|
|
57
|
+
/**
|
|
58
|
+
* Download extension từ Chrome Web Store
|
|
59
|
+
*/
|
|
60
|
+
private static downloadAndExtractChromeStoreExtension;
|
|
61
|
+
/**
|
|
62
|
+
* Download và extract extension từ URL
|
|
63
|
+
*/
|
|
64
|
+
private static downloadAndExtractExtensionFromUrl;
|
|
65
|
+
/**
|
|
66
|
+
* Get Chrome arguments để load extensions
|
|
67
|
+
*/
|
|
68
|
+
static getExtensionArgs(extensions: string[], workspacePath?: string): Promise<string[]>;
|
|
9
69
|
}
|
|
70
|
+
export {};
|
|
@@ -26,7 +26,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
26
26
|
exports.ExtensionHandler = void 0;
|
|
27
27
|
const fs = __importStar(require("fs"));
|
|
28
28
|
const path = __importStar(require("path"));
|
|
29
|
+
const https = __importStar(require("https"));
|
|
30
|
+
const http = __importStar(require("http"));
|
|
31
|
+
const zlib = __importStar(require("zlib"));
|
|
32
|
+
const crypto = __importStar(require("crypto"));
|
|
29
33
|
class ExtensionHandler {
|
|
34
|
+
/**
|
|
35
|
+
* Parse extensions từ input string (mỗi extension một dòng)
|
|
36
|
+
*/
|
|
30
37
|
static parseExtensions(extensionInput) {
|
|
31
38
|
if (!extensionInput || !extensionInput.trim()) {
|
|
32
39
|
return [];
|
|
@@ -38,49 +45,500 @@ class ExtensionHandler {
|
|
|
38
45
|
.filter(line => line.length > 0);
|
|
39
46
|
return lines;
|
|
40
47
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Xác định loại extension: Chrome Store ID, URL, hoặc Local Path
|
|
50
|
+
*/
|
|
51
|
+
static detectExtensionType(extension) {
|
|
52
|
+
// Chrome Store ID: 32 ký tự alphanumeric
|
|
53
|
+
if (/^[a-z0-9]{32}$/i.test(extension)) {
|
|
54
|
+
return 'store-id';
|
|
55
|
+
}
|
|
56
|
+
// URL: bắt đầu với http:// hoặc https://
|
|
57
|
+
if (/^https?:\/\//i.test(extension)) {
|
|
58
|
+
return 'url';
|
|
59
|
+
}
|
|
60
|
+
// Local path
|
|
61
|
+
return 'local-path';
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Validate và resolve extension path
|
|
65
|
+
*/
|
|
66
|
+
static async validateExtension(extension, workspacePath) {
|
|
67
|
+
const type = this.detectExtensionType(extension);
|
|
68
|
+
if (type === 'store-id') {
|
|
69
|
+
// Chrome Store ID - cần download
|
|
70
|
+
try {
|
|
71
|
+
const extensionPath = await this.downloadAndExtractChromeStoreExtension(extension, workspacePath);
|
|
72
|
+
return { valid: true, path: extensionPath, type: 'store-id' };
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
valid: false,
|
|
77
|
+
error: `Failed to download Chrome Store extension: ${error instanceof Error ? error.message : String(error)}`,
|
|
78
|
+
type: 'store-id',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (type === 'url') {
|
|
83
|
+
// URL - cần download
|
|
84
|
+
try {
|
|
85
|
+
const extensionPath = await this.downloadAndExtractExtensionFromUrl(extension, workspacePath);
|
|
86
|
+
return { valid: true, path: extensionPath, type: 'url' };
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
error: `Failed to download extension from URL: ${error instanceof Error ? error.message : String(error)}`,
|
|
92
|
+
type: 'url',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Local path
|
|
97
|
+
let resolvedPath;
|
|
98
|
+
if (path.isAbsolute(extension)) {
|
|
99
|
+
resolvedPath = extension;
|
|
100
|
+
}
|
|
101
|
+
else if (extension.startsWith('./') || extension.startsWith('../')) {
|
|
102
|
+
resolvedPath = path.resolve(extension);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Relative path từ workspace
|
|
106
|
+
const basePath = workspacePath || path.resolve(__dirname, '..');
|
|
107
|
+
resolvedPath = path.resolve(basePath, extension);
|
|
108
|
+
}
|
|
109
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
110
|
+
return {
|
|
111
|
+
valid: false,
|
|
112
|
+
error: `Extension path does not exist: ${resolvedPath}`,
|
|
113
|
+
type: 'local-path',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Kiểm tra xem có phải là extension directory không
|
|
117
|
+
const manifestPath = path.join(resolvedPath, 'manifest.json');
|
|
118
|
+
if (fs.existsSync(manifestPath)) {
|
|
119
|
+
return { valid: true, path: resolvedPath, type: 'local-path' };
|
|
120
|
+
}
|
|
121
|
+
// Có thể là CRX file - cần extract
|
|
122
|
+
if (resolvedPath.toLowerCase().endsWith('.crx')) {
|
|
123
|
+
try {
|
|
124
|
+
const extractedPath = await this.extractCrxFile(resolvedPath, workspacePath);
|
|
125
|
+
return { valid: true, path: extractedPath, type: 'local-path' };
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
valid: false,
|
|
130
|
+
error: `Failed to extract CRX file: ${error instanceof Error ? error.message : String(error)}`,
|
|
131
|
+
type: 'local-path',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
valid: false,
|
|
137
|
+
error: 'Extension directory does not contain manifest.json',
|
|
138
|
+
type: 'local-path',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Lấy cache directory path
|
|
143
|
+
*/
|
|
144
|
+
static getCacheDir(workspacePath) {
|
|
145
|
+
const basePath = workspacePath || path.resolve(__dirname, '..');
|
|
146
|
+
return path.join(basePath, '..', this.CACHE_DIR_NAME);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Đảm bảo cache directory tồn tại
|
|
150
|
+
*/
|
|
151
|
+
static ensureCacheDir(workspacePath) {
|
|
152
|
+
const cacheDir = this.getCacheDir(workspacePath);
|
|
153
|
+
if (!fs.existsSync(cacheDir)) {
|
|
154
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
155
|
+
}
|
|
156
|
+
return cacheDir;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Tạo cache key từ extension identifier
|
|
160
|
+
*/
|
|
161
|
+
static getCacheKey(identifier) {
|
|
162
|
+
return crypto.createHash('md5').update(identifier).digest('hex');
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Kiểm tra extension đã được cache chưa
|
|
166
|
+
*/
|
|
167
|
+
static getCachedExtensionPath(cacheKey, workspacePath) {
|
|
168
|
+
const cacheDir = this.getCacheDir(workspacePath);
|
|
169
|
+
const cachedPath = path.join(cacheDir, cacheKey);
|
|
170
|
+
const manifestPath = path.join(cachedPath, 'manifest.json');
|
|
171
|
+
if (fs.existsSync(manifestPath)) {
|
|
172
|
+
console.log(`[ExtensionHandler] Using cached extension: ${cachedPath}`);
|
|
173
|
+
return cachedPath;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Download file từ URL
|
|
179
|
+
*/
|
|
180
|
+
static async downloadFile(url, outputPath, retries = this.MAX_RETRIES) {
|
|
181
|
+
return new Promise((resolve, reject) => {
|
|
182
|
+
const attemptDownload = (downloadUrl, attempt) => {
|
|
183
|
+
console.log(`[ExtensionHandler] Downloading ${downloadUrl} (attempt ${attempt}/${retries})...`);
|
|
184
|
+
const file = fs.createWriteStream(outputPath);
|
|
185
|
+
const protocol = downloadUrl.startsWith('https:') ? https : http;
|
|
186
|
+
const request = protocol.get(downloadUrl, (response) => {
|
|
187
|
+
// Handle redirects
|
|
188
|
+
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307 || response.statusCode === 308) {
|
|
189
|
+
file.close();
|
|
190
|
+
fs.unlinkSync(outputPath);
|
|
191
|
+
if (response.headers.location) {
|
|
192
|
+
const redirectUrl = response.headers.location.startsWith('http')
|
|
193
|
+
? response.headers.location
|
|
194
|
+
: new URL(response.headers.location, downloadUrl).href;
|
|
195
|
+
return attemptDownload(redirectUrl, attempt);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (response.statusCode !== 200) {
|
|
199
|
+
file.close();
|
|
200
|
+
fs.unlinkSync(outputPath);
|
|
201
|
+
if (attempt < retries) {
|
|
202
|
+
return attemptDownload(downloadUrl, attempt + 1);
|
|
203
|
+
}
|
|
204
|
+
return reject(new Error(`Download failed with status ${response.statusCode}`));
|
|
205
|
+
}
|
|
206
|
+
response.pipe(file);
|
|
207
|
+
file.on('finish', () => {
|
|
208
|
+
file.close();
|
|
209
|
+
console.log(`[ExtensionHandler] Downloaded to ${outputPath}`);
|
|
210
|
+
resolve();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
request.on('error', (error) => {
|
|
214
|
+
file.close();
|
|
215
|
+
if (fs.existsSync(outputPath)) {
|
|
216
|
+
fs.unlinkSync(outputPath);
|
|
217
|
+
}
|
|
218
|
+
if (attempt < retries) {
|
|
219
|
+
console.log(`[ExtensionHandler] Download error, retrying... (${error.message})`);
|
|
220
|
+
setTimeout(() => attemptDownload(downloadUrl, attempt + 1), 1000 * attempt);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
reject(error);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
request.setTimeout(this.DOWNLOAD_TIMEOUT, () => {
|
|
227
|
+
request.destroy();
|
|
228
|
+
file.close();
|
|
229
|
+
if (fs.existsSync(outputPath)) {
|
|
230
|
+
fs.unlinkSync(outputPath);
|
|
231
|
+
}
|
|
232
|
+
if (attempt < retries) {
|
|
233
|
+
attemptDownload(downloadUrl, attempt + 1);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
reject(new Error('Download timeout'));
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
attemptDownload(url, 1);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Extract CRX file
|
|
245
|
+
* CRX format: Header (magic number + version + pub key len + sig len + pub key + sig) + ZIP content
|
|
246
|
+
*/
|
|
247
|
+
static async extractCrxFile(crxPath, workspacePath) {
|
|
248
|
+
const cacheKey = this.getCacheKey(`local-crx-${crxPath}`);
|
|
249
|
+
const cachedPath = this.getCachedExtensionPath(cacheKey, workspacePath);
|
|
250
|
+
if (cachedPath) {
|
|
251
|
+
return cachedPath;
|
|
252
|
+
}
|
|
253
|
+
console.log(`[ExtensionHandler] Extracting CRX file: ${crxPath}`);
|
|
254
|
+
const cacheDir = this.ensureCacheDir(workspacePath);
|
|
255
|
+
const outputDir = path.join(cacheDir, cacheKey);
|
|
256
|
+
if (fs.existsSync(outputDir)) {
|
|
257
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
260
|
+
const crxBuffer = fs.readFileSync(crxPath);
|
|
261
|
+
// Check magic number (Cr24 = 0x43723234)
|
|
262
|
+
const magic = crxBuffer.readUInt32LE(0);
|
|
263
|
+
if (magic !== 0x34327243) {
|
|
264
|
+
throw new Error('Invalid CRX file: wrong magic number');
|
|
265
|
+
}
|
|
266
|
+
// Read version (4 bytes at offset 4)
|
|
267
|
+
const version = crxBuffer.readUInt32LE(4);
|
|
268
|
+
if (version === 2) {
|
|
269
|
+
// CRX v2: magic (4) + version (4) + pub key len (4) + sig len (4) + pub key + sig + zip
|
|
270
|
+
const pubKeyLen = crxBuffer.readUInt32LE(8);
|
|
271
|
+
const sigLen = crxBuffer.readUInt32LE(12);
|
|
272
|
+
const headerSize = 16 + pubKeyLen + sigLen;
|
|
273
|
+
const zipBuffer = crxBuffer.slice(headerSize);
|
|
274
|
+
await this.extractZipBuffer(zipBuffer, outputDir);
|
|
275
|
+
}
|
|
276
|
+
else if (version === 3) {
|
|
277
|
+
// CRX v3: similar but with additional header
|
|
278
|
+
const headerSize = crxBuffer.readUInt32LE(8);
|
|
279
|
+
const zipBuffer = crxBuffer.slice(headerSize);
|
|
280
|
+
await this.extractZipBuffer(zipBuffer, outputDir);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// Try to find ZIP header manually (PK\x03\x04)
|
|
284
|
+
let zipStart = -1;
|
|
285
|
+
for (let i = 0; i < crxBuffer.length - 4; i++) {
|
|
286
|
+
if (crxBuffer[i] === 0x50 &&
|
|
287
|
+
crxBuffer[i + 1] === 0x4b &&
|
|
288
|
+
crxBuffer[i + 2] === 0x03 &&
|
|
289
|
+
crxBuffer[i + 3] === 0x04) {
|
|
290
|
+
zipStart = i;
|
|
291
|
+
break;
|
|
54
292
|
}
|
|
55
|
-
return { valid: false, error: 'Extension directory does not contain manifest.json' };
|
|
56
293
|
}
|
|
57
|
-
|
|
294
|
+
if (zipStart === -1) {
|
|
295
|
+
throw new Error('Invalid CRX file: ZIP content not found');
|
|
296
|
+
}
|
|
297
|
+
const zipBuffer = crxBuffer.slice(zipStart);
|
|
298
|
+
await this.extractZipBuffer(zipBuffer, outputDir);
|
|
299
|
+
}
|
|
300
|
+
// Validate manifest.json
|
|
301
|
+
const manifestPath = path.join(outputDir, 'manifest.json');
|
|
302
|
+
if (!fs.existsSync(manifestPath)) {
|
|
303
|
+
throw new Error('Extracted extension does not contain manifest.json');
|
|
58
304
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
305
|
+
console.log(`[ExtensionHandler] Extracted CRX to: ${outputDir}`);
|
|
306
|
+
return outputDir;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Extract ZIP buffer to directory
|
|
310
|
+
*/
|
|
311
|
+
static async extractZipBuffer(zipBuffer, outputDir) {
|
|
312
|
+
// ZIP files are not zlib-compressed, they have their own format
|
|
313
|
+
// Use manual extraction
|
|
314
|
+
return this.extractZipManually(zipBuffer, outputDir);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Manual ZIP extraction
|
|
318
|
+
* Supports stored (method 0) and deflated (method 8) compression
|
|
319
|
+
*/
|
|
320
|
+
static async extractZipManually(zipBuffer, outputDir) {
|
|
321
|
+
const files = [];
|
|
322
|
+
let offset = 0;
|
|
323
|
+
// First pass: collect all file entries
|
|
324
|
+
while (offset < zipBuffer.length - 4) {
|
|
325
|
+
// Look for local file header signature (0x04034b50 = "PK\x03\x04")
|
|
326
|
+
if (zipBuffer[offset] !== 0x50 || zipBuffer[offset + 1] !== 0x4b) {
|
|
327
|
+
offset++;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const signature = zipBuffer.readUInt32LE(offset);
|
|
331
|
+
if (signature !== 0x04034b50) {
|
|
332
|
+
offset++;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
// Read file header
|
|
336
|
+
const version = zipBuffer.readUInt16LE(offset + 4);
|
|
337
|
+
const flags = zipBuffer.readUInt16LE(offset + 6);
|
|
338
|
+
const method = zipBuffer.readUInt16LE(offset + 8);
|
|
339
|
+
const compressedSize = zipBuffer.readUInt32LE(offset + 18);
|
|
340
|
+
const uncompressedSize = zipBuffer.readUInt32LE(offset + 22);
|
|
341
|
+
const fileNameLength = zipBuffer.readUInt16LE(offset + 26);
|
|
342
|
+
const extraFieldLength = zipBuffer.readUInt16LE(offset + 28);
|
|
343
|
+
if (offset + 30 + fileNameLength + extraFieldLength > zipBuffer.length) {
|
|
344
|
+
break;
|
|
65
345
|
}
|
|
66
|
-
|
|
346
|
+
const fileName = zipBuffer
|
|
347
|
+
.slice(offset + 30, offset + 30 + fileNameLength)
|
|
348
|
+
.toString('utf-8')
|
|
349
|
+
.replace(/\\/g, '/'); // Normalize path separators
|
|
350
|
+
const dataOffset = offset + 30 + fileNameLength + extraFieldLength;
|
|
351
|
+
if (dataOffset + compressedSize > zipBuffer.length) {
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
const fileData = zipBuffer.slice(dataOffset, dataOffset + compressedSize);
|
|
355
|
+
if (!fileName.endsWith('/')) {
|
|
356
|
+
files.push({
|
|
357
|
+
name: fileName,
|
|
358
|
+
data: fileData,
|
|
359
|
+
method: method,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
offset = dataOffset + compressedSize;
|
|
363
|
+
}
|
|
364
|
+
// Second pass: extract files
|
|
365
|
+
for (const file of files) {
|
|
366
|
+
try {
|
|
367
|
+
const filePath = path.join(outputDir, file.name);
|
|
368
|
+
const dir = path.dirname(filePath);
|
|
369
|
+
// Skip if path traversal attempt
|
|
370
|
+
if (!filePath.startsWith(path.resolve(outputDir))) {
|
|
371
|
+
console.warn(`[ExtensionHandler] Skipping potentially dangerous path: ${file.name}`);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (!fs.existsSync(dir)) {
|
|
375
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
376
|
+
}
|
|
377
|
+
let extractedData;
|
|
378
|
+
if (file.method === 0) {
|
|
379
|
+
// Stored (no compression)
|
|
380
|
+
extractedData = file.data;
|
|
381
|
+
}
|
|
382
|
+
else if (file.method === 8) {
|
|
383
|
+
// Deflated - use zlib
|
|
384
|
+
try {
|
|
385
|
+
extractedData = zlib.inflateRawSync(file.data);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
console.warn(`[ExtensionHandler] Failed to inflate ${file.name}, trying stored method`);
|
|
389
|
+
extractedData = file.data;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
console.warn(`[ExtensionHandler] Unsupported compression method ${file.method} for ${file.name}, skipping`);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
fs.writeFileSync(filePath, extractedData);
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
console.warn(`[ExtensionHandler] Error extracting file ${file.name}:`, error);
|
|
400
|
+
// Continue with other files
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (files.length === 0) {
|
|
404
|
+
throw new Error('No valid files found in ZIP archive');
|
|
67
405
|
}
|
|
68
|
-
return { valid: false, error: 'Invalid extension format. Must be Chrome Store ID or valid path' };
|
|
69
406
|
}
|
|
70
|
-
|
|
407
|
+
/**
|
|
408
|
+
* Download extension từ Chrome Web Store
|
|
409
|
+
*/
|
|
410
|
+
static async downloadAndExtractChromeStoreExtension(extensionId, workspacePath) {
|
|
411
|
+
const cacheKey = this.getCacheKey(extensionId);
|
|
412
|
+
const cachedPath = this.getCachedExtensionPath(cacheKey, workspacePath);
|
|
413
|
+
if (cachedPath) {
|
|
414
|
+
return cachedPath;
|
|
415
|
+
}
|
|
416
|
+
console.log(`[ExtensionHandler] Downloading Chrome Store extension: ${extensionId}`);
|
|
417
|
+
const cacheDir = this.ensureCacheDir(workspacePath);
|
|
418
|
+
const tempCrxPath = path.join(cacheDir, `${cacheKey}.crx`);
|
|
419
|
+
const outputDir = path.join(cacheDir, cacheKey);
|
|
420
|
+
try {
|
|
421
|
+
// Chrome Web Store download URL
|
|
422
|
+
const downloadUrl = `https://clients2.google.com/service/update2/crx?response=redirect&x=id%3D${extensionId}%26uc`;
|
|
423
|
+
// Download CRX file
|
|
424
|
+
await this.downloadFile(downloadUrl, tempCrxPath);
|
|
425
|
+
// Extract CRX
|
|
426
|
+
const extractedPath = await this.extractCrxFile(tempCrxPath, workspacePath);
|
|
427
|
+
// Move to cache directory with proper name
|
|
428
|
+
if (extractedPath !== outputDir) {
|
|
429
|
+
if (fs.existsSync(outputDir)) {
|
|
430
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
431
|
+
}
|
|
432
|
+
fs.renameSync(extractedPath, outputDir);
|
|
433
|
+
}
|
|
434
|
+
// Clean up temp CRX file
|
|
435
|
+
if (fs.existsSync(tempCrxPath)) {
|
|
436
|
+
fs.unlinkSync(tempCrxPath);
|
|
437
|
+
}
|
|
438
|
+
return outputDir;
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
// Clean up on error
|
|
442
|
+
if (fs.existsSync(tempCrxPath)) {
|
|
443
|
+
fs.unlinkSync(tempCrxPath);
|
|
444
|
+
}
|
|
445
|
+
if (fs.existsSync(outputDir)) {
|
|
446
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
447
|
+
}
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Download và extract extension từ URL
|
|
453
|
+
*/
|
|
454
|
+
static async downloadAndExtractExtensionFromUrl(url, workspacePath) {
|
|
455
|
+
const cacheKey = this.getCacheKey(url);
|
|
456
|
+
const cachedPath = this.getCachedExtensionPath(cacheKey, workspacePath);
|
|
457
|
+
if (cachedPath) {
|
|
458
|
+
return cachedPath;
|
|
459
|
+
}
|
|
460
|
+
console.log(`[ExtensionHandler] Downloading extension from URL: ${url}`);
|
|
461
|
+
const cacheDir = this.ensureCacheDir(workspacePath);
|
|
462
|
+
const tempFilePath = path.join(cacheDir, `${cacheKey}.tmp`);
|
|
463
|
+
const outputDir = path.join(cacheDir, cacheKey);
|
|
464
|
+
try {
|
|
465
|
+
// Download file
|
|
466
|
+
await this.downloadFile(url, tempFilePath);
|
|
467
|
+
// Determine file type and extract
|
|
468
|
+
const fullBuffer = fs.readFileSync(tempFilePath);
|
|
469
|
+
const fileHeader = fullBuffer.slice(0, 4);
|
|
470
|
+
const headerStr = fileHeader.toString('ascii', 0, 4);
|
|
471
|
+
const isCrx = url.toLowerCase().endsWith('.crx') || headerStr === 'Cr24';
|
|
472
|
+
if (isCrx || tempFilePath.toLowerCase().endsWith('.crx')) {
|
|
473
|
+
// Extract CRX
|
|
474
|
+
const extractedPath = await this.extractCrxFile(tempFilePath, workspacePath);
|
|
475
|
+
if (extractedPath !== outputDir) {
|
|
476
|
+
if (fs.existsSync(outputDir)) {
|
|
477
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
478
|
+
}
|
|
479
|
+
fs.renameSync(extractedPath, outputDir);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
// Try as ZIP
|
|
484
|
+
const zipBuffer = fs.readFileSync(tempFilePath);
|
|
485
|
+
if (!fs.existsSync(outputDir)) {
|
|
486
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
487
|
+
}
|
|
488
|
+
await this.extractZipManually(zipBuffer, outputDir);
|
|
489
|
+
// Validate manifest.json
|
|
490
|
+
const manifestPath = path.join(outputDir, 'manifest.json');
|
|
491
|
+
if (!fs.existsSync(manifestPath)) {
|
|
492
|
+
throw new Error('Downloaded file does not contain manifest.json');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Clean up temp file
|
|
496
|
+
if (fs.existsSync(tempFilePath)) {
|
|
497
|
+
fs.unlinkSync(tempFilePath);
|
|
498
|
+
}
|
|
499
|
+
return outputDir;
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
// Clean up on error
|
|
503
|
+
if (fs.existsSync(tempFilePath)) {
|
|
504
|
+
fs.unlinkSync(tempFilePath);
|
|
505
|
+
}
|
|
506
|
+
if (fs.existsSync(outputDir)) {
|
|
507
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
508
|
+
}
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Get Chrome arguments để load extensions
|
|
514
|
+
*/
|
|
515
|
+
static async getExtensionArgs(extensions, workspacePath) {
|
|
71
516
|
const args = [];
|
|
72
517
|
const validPaths = [];
|
|
73
518
|
for (const ext of extensions) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
519
|
+
try {
|
|
520
|
+
console.log(`[ExtensionHandler] Processing extension: ${ext}`);
|
|
521
|
+
const validation = await this.validateExtension(ext, workspacePath);
|
|
522
|
+
if (validation.valid && validation.path) {
|
|
523
|
+
validPaths.push(validation.path);
|
|
524
|
+
console.log(`[ExtensionHandler] ✓ Extension validated: ${validation.path}`);
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
console.error(`[ExtensionHandler] ✗ Extension validation failed: ${validation.error}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
console.error(`[ExtensionHandler] ✗ Error processing extension "${ext}":`, error);
|
|
77
532
|
}
|
|
78
|
-
// Chrome Store IDs sẽ được load sau khi browser khởi động
|
|
79
533
|
}
|
|
80
534
|
if (validPaths.length > 0) {
|
|
81
535
|
args.push(`--load-extension=${validPaths.join(',')}`);
|
|
536
|
+
console.log(`[ExtensionHandler] Load extensions: ${validPaths.length} extension(s)`);
|
|
82
537
|
}
|
|
83
538
|
return args;
|
|
84
539
|
}
|
|
85
540
|
}
|
|
541
|
+
ExtensionHandler.CACHE_DIR_NAME = 'extensions-cache';
|
|
542
|
+
ExtensionHandler.DOWNLOAD_TIMEOUT = 30000; // 30 seconds
|
|
543
|
+
ExtensionHandler.MAX_RETRIES = 3;
|
|
86
544
|
exports.ExtensionHandler = ExtensionHandler;
|
|
@@ -5,7 +5,7 @@ export declare class ProfileManager {
|
|
|
5
5
|
private ensureProfilesDir;
|
|
6
6
|
private getProfileMetadataPath;
|
|
7
7
|
private getProfileDir;
|
|
8
|
-
createProfile(name: string, proxy?: string, note?: string, extensions?: string[]): ProfileData;
|
|
8
|
+
createProfile(name: string, proxy?: string, note?: string, extensions?: string[], extensionPaths?: string[]): ProfileData;
|
|
9
9
|
private setProfileName;
|
|
10
10
|
getProfile(profileId: string): ProfileData | null;
|
|
11
11
|
getAllProfiles(): ProfileData[];
|
|
@@ -43,7 +43,7 @@ class ProfileManager {
|
|
|
43
43
|
getProfileDir(profileId) {
|
|
44
44
|
return path.join(this.profilesDir, profileId);
|
|
45
45
|
}
|
|
46
|
-
createProfile(name, proxy, note, extensions) {
|
|
46
|
+
createProfile(name, proxy, note, extensions, extensionPaths) {
|
|
47
47
|
const profileId = (0, uuid_1.v4)();
|
|
48
48
|
const profileDir = this.getProfileDir(profileId);
|
|
49
49
|
// Tạo thư mục profile
|
|
@@ -54,6 +54,7 @@ class ProfileManager {
|
|
|
54
54
|
proxy,
|
|
55
55
|
note,
|
|
56
56
|
extensions: extensions || [],
|
|
57
|
+
extensionPaths: extensionPaths || [],
|
|
57
58
|
createdAt: new Date().toISOString(),
|
|
58
59
|
updatedAt: new Date().toISOString(),
|
|
59
60
|
};
|
package/dist/utils/types.d.ts
CHANGED
package/package.json
CHANGED