n8n-nodes-dhd-browser 1.0.0
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/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# n8n-nodes-dhd-browser
|
|
2
|
+
|
|
3
|
+
n8n community node for creating and launching browser profiles with automatic proxy authentication.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ Create browser profiles with fingerprint configuration
|
|
8
|
+
- ✅ Parse proxy strings or use separate proxy fields
|
|
9
|
+
- ✅ Automatic proxy authentication injection (no manual login required)
|
|
10
|
+
- ✅ Launch Chromium/Chrome with custom profiles
|
|
11
|
+
- ✅ Support for fingerprinting (OS, Chrome version, User Agent, Timezone, etc.)
|
|
12
|
+
- ✅ Modify Chrome Preferences automatically
|
|
13
|
+
- ✅ Inject proxy authentication via Chrome DevTools Protocol
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install n8n-nodes-dhd-browser
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## How It Works
|
|
22
|
+
|
|
23
|
+
This node implements the same approach used by professional browser automation tools:
|
|
24
|
+
|
|
25
|
+
1. **Create Profile**:
|
|
26
|
+
- Parses proxy configuration (from string or separate fields)
|
|
27
|
+
- Generates browser fingerprint
|
|
28
|
+
- Creates profile directory structure
|
|
29
|
+
- Modifies Chrome Preferences to set proxy server
|
|
30
|
+
- Saves profile configuration JSON
|
|
31
|
+
|
|
32
|
+
2. **Launch Profile**:
|
|
33
|
+
- Loads profile configuration
|
|
34
|
+
- Launches Chromium with `--proxy-server` flag
|
|
35
|
+
- Connects via Chrome DevTools Protocol
|
|
36
|
+
- Injects `Proxy-Authorization` header automatically via `Network.setExtraHTTPHeaders()`
|
|
37
|
+
- Browser is ready to use without manual proxy login
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Create Profile
|
|
42
|
+
|
|
43
|
+
1. Set **Operation** to "Create Profile"
|
|
44
|
+
2. Provide:
|
|
45
|
+
- **Profile Name**: Name for your profile
|
|
46
|
+
- **Profile Directory**: Base directory where profiles will be stored
|
|
47
|
+
- **Proxy Configuration**: Choose "Proxy String" or "Separate Fields"
|
|
48
|
+
- **Browser Fingerprint**: Configure OS, Chrome version, User Agent, etc.
|
|
49
|
+
|
|
50
|
+
3. Output: Profile configuration path and details
|
|
51
|
+
|
|
52
|
+
### Launch Profile
|
|
53
|
+
|
|
54
|
+
1. Set **Operation** to "Launch Profile"
|
|
55
|
+
2. Provide:
|
|
56
|
+
- **Profile Config Path**: Path to the `profile.config.json` file created by "Create Profile"
|
|
57
|
+
- **Chrome Executable Path**: Full path to Chrome/Chromium executable
|
|
58
|
+
- **Headless**: Whether to run in headless mode
|
|
59
|
+
- **Window Size**: Browser window size (e.g., "1920x1080")
|
|
60
|
+
|
|
61
|
+
3. Output: Browser connection information (WebSocket endpoint, debug port)
|
|
62
|
+
|
|
63
|
+
## Proxy Format
|
|
64
|
+
|
|
65
|
+
### Proxy String Format
|
|
66
|
+
```
|
|
67
|
+
http://username:password@proxy.example.com:8080
|
|
68
|
+
socks5://user:pass@192.168.1.1:1080
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Separate Fields
|
|
72
|
+
- Proxy Type: http, https, socks4, socks5
|
|
73
|
+
- Proxy Host: IP address or hostname
|
|
74
|
+
- Proxy Port: Port number
|
|
75
|
+
- Proxy Username: Authentication username
|
|
76
|
+
- Proxy Password: Authentication password
|
|
77
|
+
|
|
78
|
+
## Browser Fingerprint
|
|
79
|
+
|
|
80
|
+
Configure fingerprint settings:
|
|
81
|
+
- **OS**: Windows, macOS, Linux
|
|
82
|
+
- **Chrome Version**: Chrome version to emulate (e.g., "120.0.0.0")
|
|
83
|
+
- **User Agent**: Custom user agent (auto-generated if empty)
|
|
84
|
+
- **Screen Resolution**: e.g., "1920x1080"
|
|
85
|
+
- **Timezone**: e.g., "America/New_York", "Asia/Ho_Chi_Minh"
|
|
86
|
+
- **Language**: Browser language, e.g., "en-US", "vi-VN"
|
|
87
|
+
|
|
88
|
+
## Technical Details
|
|
89
|
+
|
|
90
|
+
### Automatic Proxy Authentication
|
|
91
|
+
|
|
92
|
+
The node uses Chrome DevTools Protocol to inject proxy authentication:
|
|
93
|
+
|
|
94
|
+
1. Browser launches with `--proxy-server` flag
|
|
95
|
+
2. Node connects to browser via DevTools Protocol (debug port)
|
|
96
|
+
3. Enables Network domain: `Network.enable()`
|
|
97
|
+
4. Sets extra HTTP headers: `Network.setExtraHTTPHeaders()` with `Proxy-Authorization: Basic base64(user:pass)`
|
|
98
|
+
5. All requests automatically include authentication header
|
|
99
|
+
|
|
100
|
+
This approach ensures:
|
|
101
|
+
- ✅ No manual proxy login dialog
|
|
102
|
+
- ✅ Authentication persists for entire browser session
|
|
103
|
+
- ✅ Works with HTTP, HTTPS, SOCKS4, and SOCKS5 proxies
|
|
104
|
+
|
|
105
|
+
### Chrome Preferences
|
|
106
|
+
|
|
107
|
+
The node automatically modifies Chrome Preferences file to:
|
|
108
|
+
- Set proxy server configuration
|
|
109
|
+
- Store fingerprint settings
|
|
110
|
+
- Configure default browser behavior
|
|
111
|
+
|
|
112
|
+
## Requirements
|
|
113
|
+
|
|
114
|
+
- Node.js >= 18
|
|
115
|
+
- Chrome or Chromium browser installed
|
|
116
|
+
- n8n >= 1.0.0
|
|
117
|
+
|
|
118
|
+
## Dependencies
|
|
119
|
+
|
|
120
|
+
- `puppeteer-core`: Browser automation
|
|
121
|
+
- `chrome-remote-interface`: Chrome DevTools Protocol client
|
|
122
|
+
- `fs-extra`: File system operations
|
|
123
|
+
- `uuid`: Generate unique profile IDs
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
|
128
|
+
|
|
129
|
+
## Author
|
|
130
|
+
|
|
131
|
+
DHD
|
|
132
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class BrowserProfile implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
private createProfile;
|
|
6
|
+
private launchProfile;
|
|
7
|
+
private parseProxyString;
|
|
8
|
+
private generateFingerprint;
|
|
9
|
+
private modifyChromePreferences;
|
|
10
|
+
private injectProxyAuthentication;
|
|
11
|
+
private findAvailablePort;
|
|
12
|
+
}
|
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.BrowserProfile = void 0;
|
|
30
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
31
|
+
const puppeteer = __importStar(require("puppeteer-core"));
|
|
32
|
+
const fs = __importStar(require("fs-extra"));
|
|
33
|
+
const path = __importStar(require("path"));
|
|
34
|
+
const net = __importStar(require("net"));
|
|
35
|
+
const uuid_1 = require("uuid");
|
|
36
|
+
// @ts-expect-error - chrome-remote-interface doesn't have TypeScript definitions
|
|
37
|
+
const chrome_remote_interface_1 = __importDefault(require("chrome-remote-interface"));
|
|
38
|
+
class BrowserProfile {
|
|
39
|
+
constructor() {
|
|
40
|
+
this.description = {
|
|
41
|
+
displayName: 'Browser Profile',
|
|
42
|
+
name: 'browserProfile',
|
|
43
|
+
icon: 'file:browser.svg',
|
|
44
|
+
group: ['transform'],
|
|
45
|
+
version: 1,
|
|
46
|
+
subtitle: '={{$parameter["operation"]}}',
|
|
47
|
+
description: 'Create and launch browser profile with automatic proxy authentication',
|
|
48
|
+
defaults: {
|
|
49
|
+
name: 'Browser Profile',
|
|
50
|
+
},
|
|
51
|
+
inputs: ['main'],
|
|
52
|
+
outputs: ['main'],
|
|
53
|
+
credentials: [],
|
|
54
|
+
properties: [
|
|
55
|
+
{
|
|
56
|
+
displayName: 'Operation',
|
|
57
|
+
name: 'operation',
|
|
58
|
+
type: 'options',
|
|
59
|
+
noDataExpression: true,
|
|
60
|
+
options: [
|
|
61
|
+
{
|
|
62
|
+
name: 'Create Profile',
|
|
63
|
+
value: 'create',
|
|
64
|
+
description: 'Create a new browser profile with fingerprint and proxy config',
|
|
65
|
+
action: 'Create a new browser profile',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'Launch Profile',
|
|
69
|
+
value: 'launch',
|
|
70
|
+
description: 'Launch an existing browser profile with proxy authentication',
|
|
71
|
+
action: 'Launch an existing browser profile',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
default: 'create',
|
|
75
|
+
},
|
|
76
|
+
// CREATE PROFILE OPTIONS
|
|
77
|
+
{
|
|
78
|
+
displayName: 'Profile Name',
|
|
79
|
+
name: 'profileName',
|
|
80
|
+
type: 'string',
|
|
81
|
+
default: '',
|
|
82
|
+
required: true,
|
|
83
|
+
displayOptions: {
|
|
84
|
+
show: {
|
|
85
|
+
operation: ['create'],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
description: 'Name of the browser profile',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
displayName: 'Profile Directory',
|
|
92
|
+
name: 'profileDir',
|
|
93
|
+
type: 'string',
|
|
94
|
+
default: '',
|
|
95
|
+
required: true,
|
|
96
|
+
displayOptions: {
|
|
97
|
+
show: {
|
|
98
|
+
operation: ['create'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
description: 'Directory path to store the profile (will be created if not exists)',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
displayName: 'Proxy Configuration',
|
|
105
|
+
name: 'proxyConfig',
|
|
106
|
+
type: 'options',
|
|
107
|
+
default: 'string',
|
|
108
|
+
options: [
|
|
109
|
+
{
|
|
110
|
+
name: 'Proxy String',
|
|
111
|
+
value: 'string',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'Separate Fields',
|
|
115
|
+
value: 'fields',
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
displayOptions: {
|
|
119
|
+
show: {
|
|
120
|
+
operation: ['create'],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
description: 'How to provide proxy configuration',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
displayName: 'Proxy String',
|
|
127
|
+
name: 'proxyString',
|
|
128
|
+
type: 'string',
|
|
129
|
+
default: '',
|
|
130
|
+
placeholder: 'http://username:password@proxy.example.com:8080',
|
|
131
|
+
displayOptions: {
|
|
132
|
+
show: {
|
|
133
|
+
operation: ['create'],
|
|
134
|
+
proxyConfig: ['string'],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
description: 'Proxy string in format: type://user:pass@host:port',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
displayName: 'Proxy Type',
|
|
141
|
+
name: 'proxyType',
|
|
142
|
+
type: 'options',
|
|
143
|
+
options: [
|
|
144
|
+
{
|
|
145
|
+
name: 'HTTP',
|
|
146
|
+
value: 'http',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'HTTPS',
|
|
150
|
+
value: 'https',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'SOCKS4',
|
|
154
|
+
value: 'socks4',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'SOCKS5',
|
|
158
|
+
value: 'socks5',
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
default: 'http',
|
|
162
|
+
displayOptions: {
|
|
163
|
+
show: {
|
|
164
|
+
operation: ['create'],
|
|
165
|
+
proxyConfig: ['fields'],
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
description: 'Type of proxy server',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
displayName: 'Proxy Host',
|
|
172
|
+
name: 'proxyHost',
|
|
173
|
+
type: 'string',
|
|
174
|
+
default: '',
|
|
175
|
+
displayOptions: {
|
|
176
|
+
show: {
|
|
177
|
+
operation: ['create'],
|
|
178
|
+
proxyConfig: ['fields'],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
description: 'Proxy server hostname or IP address',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
displayName: 'Proxy Port',
|
|
185
|
+
name: 'proxyPort',
|
|
186
|
+
type: 'number',
|
|
187
|
+
default: 8080,
|
|
188
|
+
displayOptions: {
|
|
189
|
+
show: {
|
|
190
|
+
operation: ['create'],
|
|
191
|
+
proxyConfig: ['fields'],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
description: 'Proxy server port',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
displayName: 'Proxy Username',
|
|
198
|
+
name: 'proxyUser',
|
|
199
|
+
type: 'string',
|
|
200
|
+
default: '',
|
|
201
|
+
displayOptions: {
|
|
202
|
+
show: {
|
|
203
|
+
operation: ['create'],
|
|
204
|
+
proxyConfig: ['fields'],
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
description: 'Username for proxy authentication',
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
displayName: 'Proxy Password',
|
|
211
|
+
name: 'proxyPass',
|
|
212
|
+
type: 'string',
|
|
213
|
+
typeOptions: {
|
|
214
|
+
password: true,
|
|
215
|
+
},
|
|
216
|
+
default: '',
|
|
217
|
+
displayOptions: {
|
|
218
|
+
show: {
|
|
219
|
+
operation: ['create'],
|
|
220
|
+
proxyConfig: ['fields'],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
description: 'Password for proxy authentication',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
displayName: 'Browser Fingerprint',
|
|
227
|
+
name: 'fingerprint',
|
|
228
|
+
type: 'fixedCollection',
|
|
229
|
+
typeOptions: {
|
|
230
|
+
multipleValues: false,
|
|
231
|
+
},
|
|
232
|
+
default: {},
|
|
233
|
+
displayOptions: {
|
|
234
|
+
show: {
|
|
235
|
+
operation: ['create'],
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
description: 'Browser fingerprint configuration',
|
|
239
|
+
options: [
|
|
240
|
+
{
|
|
241
|
+
displayName: 'Values',
|
|
242
|
+
name: 'values',
|
|
243
|
+
values: [
|
|
244
|
+
{
|
|
245
|
+
displayName: 'Chrome Version',
|
|
246
|
+
name: 'chromeVersion',
|
|
247
|
+
type: 'string',
|
|
248
|
+
default: '120.0.0.0',
|
|
249
|
+
description: 'Chrome version to emulate',
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
displayName: 'Language',
|
|
253
|
+
name: 'language',
|
|
254
|
+
type: 'string',
|
|
255
|
+
default: 'en-US',
|
|
256
|
+
description: 'Browser language (e.g., en-US, vi-VN)',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
displayName: 'OS',
|
|
260
|
+
name: 'os',
|
|
261
|
+
type: 'options',
|
|
262
|
+
options: [
|
|
263
|
+
{ name: 'Windows 10', value: 'win10' },
|
|
264
|
+
{ name: 'Windows 11', value: 'win' },
|
|
265
|
+
{ name: 'macOS', value: 'mac' },
|
|
266
|
+
{ name: 'Linux', value: 'linux' },
|
|
267
|
+
],
|
|
268
|
+
default: 'win',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
displayName: 'Screen Resolution',
|
|
272
|
+
name: 'resolution',
|
|
273
|
+
type: 'string',
|
|
274
|
+
default: '1920x1080',
|
|
275
|
+
description: 'Screen resolution (widthxheight)',
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
displayName: 'Timezone',
|
|
279
|
+
name: 'timezone',
|
|
280
|
+
type: 'string',
|
|
281
|
+
default: 'America/New_York',
|
|
282
|
+
description: 'Timezone (e.g., America/New_York, Asia/Ho_Chi_Minh)',
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
displayName: 'User Agent',
|
|
286
|
+
name: 'userAgent',
|
|
287
|
+
type: 'string',
|
|
288
|
+
default: '',
|
|
289
|
+
description: 'Custom user agent (leave empty to auto-generate)',
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
// LAUNCH PROFILE OPTIONS
|
|
296
|
+
{
|
|
297
|
+
displayName: 'Profile Config Path',
|
|
298
|
+
name: 'profileConfigPath',
|
|
299
|
+
type: 'string',
|
|
300
|
+
default: '',
|
|
301
|
+
required: true,
|
|
302
|
+
displayOptions: {
|
|
303
|
+
show: {
|
|
304
|
+
operation: ['launch'],
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
description: 'Path to profile config JSON file (created by Create Profile operation)',
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
displayName: 'Chrome Executable Path',
|
|
311
|
+
name: 'chromePath',
|
|
312
|
+
type: 'string',
|
|
313
|
+
default: '',
|
|
314
|
+
required: true,
|
|
315
|
+
displayOptions: {
|
|
316
|
+
show: {
|
|
317
|
+
operation: ['launch'],
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
description: 'Path to Chrome/Chromium executable',
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
displayName: 'Headless',
|
|
324
|
+
name: 'headless',
|
|
325
|
+
type: 'boolean',
|
|
326
|
+
default: false,
|
|
327
|
+
displayOptions: {
|
|
328
|
+
show: {
|
|
329
|
+
operation: ['launch'],
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
description: 'Whether to run browser in headless mode',
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
displayName: 'Window Size',
|
|
336
|
+
name: 'windowSize',
|
|
337
|
+
type: 'string',
|
|
338
|
+
default: '1920x1080',
|
|
339
|
+
displayOptions: {
|
|
340
|
+
show: {
|
|
341
|
+
operation: ['launch'],
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
description: 'Browser window size (widthxheight)',
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
displayName: 'Return Browser Info',
|
|
348
|
+
name: 'returnBrowserInfo',
|
|
349
|
+
type: 'boolean',
|
|
350
|
+
default: true,
|
|
351
|
+
displayOptions: {
|
|
352
|
+
show: {
|
|
353
|
+
operation: ['launch'],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
description: 'Whether to return browser connection information in output',
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
async execute() {
|
|
362
|
+
const items = this.getInputData();
|
|
363
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
364
|
+
const returnData = [];
|
|
365
|
+
// Create instance helper to access private methods
|
|
366
|
+
const instance = new BrowserProfile();
|
|
367
|
+
for (let i = 0; i < items.length; i++) {
|
|
368
|
+
try {
|
|
369
|
+
if (operation === 'create') {
|
|
370
|
+
const result = await instance.createProfile(this, i);
|
|
371
|
+
returnData.push({
|
|
372
|
+
json: result,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
else if (operation === 'launch') {
|
|
376
|
+
const result = await instance.launchProfile(this, i);
|
|
377
|
+
returnData.push({
|
|
378
|
+
json: result,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
if (this.continueOnFail()) {
|
|
384
|
+
returnData.push({
|
|
385
|
+
json: {
|
|
386
|
+
error: error instanceof Error ? error.message : String(error),
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return [returnData];
|
|
395
|
+
}
|
|
396
|
+
async createProfile(context, itemIndex) {
|
|
397
|
+
const profileName = context.getNodeParameter('profileName', itemIndex);
|
|
398
|
+
const profileDir = context.getNodeParameter('profileDir', itemIndex);
|
|
399
|
+
const proxyConfig = context.getNodeParameter('proxyConfig', itemIndex);
|
|
400
|
+
// Parse proxy configuration
|
|
401
|
+
let proxyData;
|
|
402
|
+
if (proxyConfig === 'string') {
|
|
403
|
+
const proxyString = context.getNodeParameter('proxyString', itemIndex);
|
|
404
|
+
proxyData = this.parseProxyString(context, proxyString);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
proxyData = {
|
|
408
|
+
type: context.getNodeParameter('proxyType', itemIndex),
|
|
409
|
+
host: context.getNodeParameter('proxyHost', itemIndex),
|
|
410
|
+
port: context.getNodeParameter('proxyPort', itemIndex),
|
|
411
|
+
username: context.getNodeParameter('proxyUser', itemIndex),
|
|
412
|
+
password: context.getNodeParameter('proxyPass', itemIndex),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
// Get fingerprint configuration
|
|
416
|
+
const fingerprintData = context.getNodeParameter('fingerprint.values', itemIndex, {});
|
|
417
|
+
// Generate fingerprint if needed
|
|
418
|
+
const fingerprint = this.generateFingerprint(fingerprintData);
|
|
419
|
+
// Create profile directory
|
|
420
|
+
const fullProfileDir = path.resolve(profileDir, profileName);
|
|
421
|
+
await fs.ensureDir(fullProfileDir);
|
|
422
|
+
await fs.ensureDir(path.join(fullProfileDir, 'Default'));
|
|
423
|
+
// Create profile config
|
|
424
|
+
const profileId = (0, uuid_1.v4)();
|
|
425
|
+
const profileConfig = {
|
|
426
|
+
id: profileId,
|
|
427
|
+
name: profileName,
|
|
428
|
+
profileDir: fullProfileDir,
|
|
429
|
+
proxy: {
|
|
430
|
+
type: proxyData.type,
|
|
431
|
+
host: proxyData.host,
|
|
432
|
+
port: proxyData.port,
|
|
433
|
+
username: proxyData.username,
|
|
434
|
+
password: proxyData.password,
|
|
435
|
+
},
|
|
436
|
+
fingerprint: fingerprint,
|
|
437
|
+
createdAt: new Date().toISOString(),
|
|
438
|
+
};
|
|
439
|
+
// Save profile config
|
|
440
|
+
const configPath = path.join(fullProfileDir, 'profile.config.json');
|
|
441
|
+
await fs.writeJSON(configPath, profileConfig, { spaces: 2 });
|
|
442
|
+
// Modify Chrome Preferences to set proxy
|
|
443
|
+
await this.modifyChromePreferences(fullProfileDir, proxyData);
|
|
444
|
+
return {
|
|
445
|
+
success: true,
|
|
446
|
+
profileId: profileId,
|
|
447
|
+
profileName: profileName,
|
|
448
|
+
profileDir: fullProfileDir,
|
|
449
|
+
configPath: configPath,
|
|
450
|
+
proxy: {
|
|
451
|
+
type: proxyData.type,
|
|
452
|
+
host: proxyData.host,
|
|
453
|
+
port: proxyData.port,
|
|
454
|
+
},
|
|
455
|
+
fingerprint: fingerprint,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
async launchProfile(context, itemIndex) {
|
|
459
|
+
const configPath = context.getNodeParameter('profileConfigPath', itemIndex);
|
|
460
|
+
const chromePath = context.getNodeParameter('chromePath', itemIndex);
|
|
461
|
+
const headless = context.getNodeParameter('headless', itemIndex);
|
|
462
|
+
const windowSize = context.getNodeParameter('windowSize', itemIndex);
|
|
463
|
+
const returnBrowserInfo = context.getNodeParameter('returnBrowserInfo', itemIndex);
|
|
464
|
+
// Load profile config
|
|
465
|
+
const profileConfig = await fs.readJSON(configPath);
|
|
466
|
+
const { profileDir, proxy, fingerprint } = profileConfig;
|
|
467
|
+
// Parse window size
|
|
468
|
+
const [width, height] = windowSize.split('x').map(Number);
|
|
469
|
+
// Find available debug port
|
|
470
|
+
const debugPort = await this.findAvailablePort(context, 9222);
|
|
471
|
+
// Build proxy server string
|
|
472
|
+
const proxyServer = `${proxy.type}://${proxy.host}:${proxy.port}`;
|
|
473
|
+
// Launch browser with puppeteer
|
|
474
|
+
const browser = await puppeteer.launch({
|
|
475
|
+
executablePath: chromePath,
|
|
476
|
+
headless: headless,
|
|
477
|
+
userDataDir: profileDir,
|
|
478
|
+
args: [
|
|
479
|
+
`--proxy-server=${proxyServer}`,
|
|
480
|
+
`--remote-debugging-port=${debugPort}`,
|
|
481
|
+
`--window-size=${width},${height}`,
|
|
482
|
+
'--no-sandbox',
|
|
483
|
+
'--disable-setuid-sandbox',
|
|
484
|
+
'--disable-dev-shm-usage',
|
|
485
|
+
'--disable-blink-features=AutomationControlled',
|
|
486
|
+
],
|
|
487
|
+
defaultViewport: {
|
|
488
|
+
width: width,
|
|
489
|
+
height: height,
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
// Get browser pages
|
|
493
|
+
const pages = await browser.pages();
|
|
494
|
+
const page = pages[0] || (await browser.newPage());
|
|
495
|
+
// Set user agent and other fingerprint settings
|
|
496
|
+
if (fingerprint.userAgent) {
|
|
497
|
+
await page.setUserAgent(fingerprint.userAgent);
|
|
498
|
+
}
|
|
499
|
+
if (fingerprint.timezone) {
|
|
500
|
+
await page.emulateTimezone(fingerprint.timezone);
|
|
501
|
+
}
|
|
502
|
+
// Set extra HTTP headers including proxy authentication
|
|
503
|
+
const extraHeaders = {};
|
|
504
|
+
if (fingerprint.language) {
|
|
505
|
+
extraHeaders['Accept-Language'] = fingerprint.language;
|
|
506
|
+
}
|
|
507
|
+
// Add proxy authentication header
|
|
508
|
+
if (proxy.username && proxy.password) {
|
|
509
|
+
const credentials = Buffer.from(`${proxy.username}:${proxy.password}`).toString('base64');
|
|
510
|
+
extraHeaders['Proxy-Authorization'] = `Basic ${credentials}`;
|
|
511
|
+
}
|
|
512
|
+
if (Object.keys(extraHeaders).length > 0) {
|
|
513
|
+
await page.setExtraHTTPHeaders(extraHeaders);
|
|
514
|
+
}
|
|
515
|
+
// Also inject via DevTools Protocol for maximum compatibility
|
|
516
|
+
if (proxy.username && proxy.password) {
|
|
517
|
+
await this.injectProxyAuthentication(context, debugPort, proxy.username, proxy.password);
|
|
518
|
+
}
|
|
519
|
+
// Get browser info
|
|
520
|
+
const browserInfo = {
|
|
521
|
+
success: true,
|
|
522
|
+
profileId: profileConfig.id,
|
|
523
|
+
profileName: profileConfig.name,
|
|
524
|
+
debugPort: debugPort,
|
|
525
|
+
wsEndpoint: browser.wsEndpoint(),
|
|
526
|
+
};
|
|
527
|
+
if (returnBrowserInfo) {
|
|
528
|
+
return browserInfo;
|
|
529
|
+
}
|
|
530
|
+
// Return basic info and keep browser running
|
|
531
|
+
return {
|
|
532
|
+
success: true,
|
|
533
|
+
message: 'Browser launched successfully with proxy authentication',
|
|
534
|
+
profileId: profileConfig.id,
|
|
535
|
+
debugPort: debugPort,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
parseProxyString(context, proxyString) {
|
|
539
|
+
// Parse proxy string format: type://user:pass@host:port or type://host:port
|
|
540
|
+
const urlMatch = proxyString.match(/^(\w+):\/\/(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$/);
|
|
541
|
+
if (!urlMatch) {
|
|
542
|
+
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Invalid proxy string format: ${proxyString}. Expected format: type://user:pass@host:port or type://host:port`);
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
type: urlMatch[1],
|
|
546
|
+
username: urlMatch[2] || '',
|
|
547
|
+
password: urlMatch[3] || '',
|
|
548
|
+
host: urlMatch[4],
|
|
549
|
+
port: parseInt(urlMatch[5], 10),
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
generateFingerprint(fingerprintData) {
|
|
553
|
+
const os = fingerprintData.os || 'win';
|
|
554
|
+
const chromeVersion = fingerprintData.chromeVersion || '120.0.0.0';
|
|
555
|
+
const resolution = fingerprintData.resolution || '1920x1080';
|
|
556
|
+
// Generate user agent based on OS and Chrome version
|
|
557
|
+
let userAgent = fingerprintData.userAgent;
|
|
558
|
+
if (!userAgent) {
|
|
559
|
+
if (os === 'win') {
|
|
560
|
+
userAgent = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
|
561
|
+
}
|
|
562
|
+
else if (os === 'mac') {
|
|
563
|
+
userAgent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
userAgent = `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
os: os,
|
|
571
|
+
chromeVersion: chromeVersion,
|
|
572
|
+
userAgent: userAgent,
|
|
573
|
+
resolution: resolution,
|
|
574
|
+
timezone: (fingerprintData.timezone || 'America/New_York'),
|
|
575
|
+
language: (fingerprintData.language || 'en-US'),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
async modifyChromePreferences(profileDir, proxyData) {
|
|
579
|
+
const prefsPath = path.join(profileDir, 'Default', 'Preferences');
|
|
580
|
+
let preferences = {};
|
|
581
|
+
// Try to read existing preferences
|
|
582
|
+
try {
|
|
583
|
+
if (await fs.pathExists(prefsPath)) {
|
|
584
|
+
const prefs = await fs.readJSON(prefsPath);
|
|
585
|
+
preferences = prefs;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
// If file doesn't exist or is invalid, create new preferences
|
|
590
|
+
preferences = {};
|
|
591
|
+
}
|
|
592
|
+
// Set proxy configuration in preferences
|
|
593
|
+
const proxyServer = `${proxyData.type}://${proxyData.host}:${proxyData.port}`;
|
|
594
|
+
preferences.proxy = {
|
|
595
|
+
mode: 'fixed_servers',
|
|
596
|
+
server: proxyServer,
|
|
597
|
+
bypass_list: [],
|
|
598
|
+
};
|
|
599
|
+
// Set fingerprint-related preferences
|
|
600
|
+
if (!preferences.profile) {
|
|
601
|
+
preferences.profile = {};
|
|
602
|
+
}
|
|
603
|
+
// Set default page zoom
|
|
604
|
+
preferences.default_zoom_level = 0;
|
|
605
|
+
// Save preferences
|
|
606
|
+
await fs.writeJSON(prefsPath, preferences, { spaces: 2 });
|
|
607
|
+
}
|
|
608
|
+
async injectProxyAuthentication(context, debugPort, username, password) {
|
|
609
|
+
if (!username || !password) {
|
|
610
|
+
// No authentication needed
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
// Wait for browser to be fully ready
|
|
615
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
616
|
+
// Connect to Chrome DevTools Protocol
|
|
617
|
+
const client = await (0, chrome_remote_interface_1.default)({ port: debugPort });
|
|
618
|
+
try {
|
|
619
|
+
// Enable Network domain
|
|
620
|
+
await client.Network.enable();
|
|
621
|
+
// Create base64 encoded credentials
|
|
622
|
+
const credentials = Buffer.from(`${username}:${password}`).toString('base64');
|
|
623
|
+
const authHeader = `Basic ${credentials}`;
|
|
624
|
+
// Set extra HTTP headers with Proxy-Authorization
|
|
625
|
+
// This is the primary method - adds the header to all outgoing requests
|
|
626
|
+
await client.Network.setExtraHTTPHeaders({
|
|
627
|
+
headers: {
|
|
628
|
+
'Proxy-Authorization': authHeader,
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
// Keep client reference to prevent garbage collection
|
|
632
|
+
// Store it in a way that keeps the connection alive
|
|
633
|
+
// The connection will be maintained as long as the browser is running
|
|
634
|
+
// Note: We don't close the client here because:
|
|
635
|
+
// 1. The connection needs to stay alive for the proxy auth to work
|
|
636
|
+
// 2. The browser process will clean up when it closes
|
|
637
|
+
// 3. Closing it immediately would stop the proxy authentication
|
|
638
|
+
}
|
|
639
|
+
catch (cdpError) {
|
|
640
|
+
// Try to close client if there's an error during setup
|
|
641
|
+
try {
|
|
642
|
+
await client.close();
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
// Ignore close errors
|
|
646
|
+
}
|
|
647
|
+
throw cdpError;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
// If CDP connection fails, throw error - proxy auth won't work without it
|
|
652
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
653
|
+
throw new n8n_workflow_1.NodeApiError(context.getNode(), {
|
|
654
|
+
message: `Failed to inject proxy authentication via Chrome DevTools Protocol: ${errorMessage}. Make sure the browser is running and debug port ${debugPort} is accessible.`,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async findAvailablePort(context, startPort) {
|
|
659
|
+
for (let port = startPort; port < startPort + 100; port++) {
|
|
660
|
+
const isAvailable = await new Promise((resolve) => {
|
|
661
|
+
const server = net.createServer();
|
|
662
|
+
server.listen(port, () => {
|
|
663
|
+
server.close(() => resolve(true));
|
|
664
|
+
});
|
|
665
|
+
server.on('error', () => resolve(false));
|
|
666
|
+
});
|
|
667
|
+
if (isAvailable) {
|
|
668
|
+
return port;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Could not find available port for debugging. Please try again or check if ports are in use.');
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
exports.BrowserProfile = BrowserProfile;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<rect x="2" y="4" width="20" height="16" rx="2"/>
|
|
3
|
+
<path d="M2 8h20"/>
|
|
4
|
+
<circle cx="6" cy="6" r="1" fill="currentColor"/>
|
|
5
|
+
<circle cx="9" cy="6" r="1" fill="currentColor"/>
|
|
6
|
+
</svg>
|
|
7
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-dhd-browser",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "n8n node for creating and launching browser profiles with automatic proxy authentication",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node-package",
|
|
7
|
+
"browser",
|
|
8
|
+
"profile",
|
|
9
|
+
"proxy",
|
|
10
|
+
"chromium",
|
|
11
|
+
"automation"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"homepage": "",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "DHD",
|
|
17
|
+
"email": ""
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": ""
|
|
22
|
+
},
|
|
23
|
+
"main": "dist/BrowserProfile.node.js",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc && gulp && gulp move:files",
|
|
26
|
+
"dev": "tsc --watch",
|
|
27
|
+
"format": "prettier nodes credentials --write",
|
|
28
|
+
"lint": "eslint nodes package.json --ignore-pattern credentials",
|
|
29
|
+
"lintfix": "eslint nodes package.json --ignore-pattern credentials --fix",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"n8n": {
|
|
36
|
+
"n8nNodesApiVersion": 1,
|
|
37
|
+
"nodes": [
|
|
38
|
+
"dist/nodes/BrowserProfile/BrowserProfile.node.js"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/fs-extra": "^11.0.4",
|
|
43
|
+
"@types/node": "^20.10.0",
|
|
44
|
+
"@types/uuid": "^9.0.8",
|
|
45
|
+
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
|
46
|
+
"@typescript-eslint/parser": "^6.15.0",
|
|
47
|
+
"eslint-plugin-n8n-nodes-base": "~1.12.0",
|
|
48
|
+
"gulp": "^4.0.2",
|
|
49
|
+
"n8n-workflow": "*",
|
|
50
|
+
"prettier": "^3.1.1",
|
|
51
|
+
"typescript": "~5.3.2"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"chrome-remote-interface": "^0.33.0",
|
|
55
|
+
"fs-extra": "^11.2.0",
|
|
56
|
+
"puppeteer-core": "^22.8.1",
|
|
57
|
+
"uuid": "^9.0.1"
|
|
58
|
+
}
|
|
59
|
+
}
|