@pmtuan0206/n8n-nodes-netproxy-cur 0.1.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,171 @@
1
+ # n8n-nodes-netproxy
2
+
3
+ An n8n community node for routing HTTP requests through residential proxies with automatic rotation and failover capabilities.
4
+
5
+ ## Features
6
+
7
+ - **Multi-Format Proxy Support**: Automatically parses proxies in multiple formats:
8
+ - `socks5://user:pass@host:port`
9
+ - `http://user:pass@host:port`
10
+ - `https://user:pass@host:port`
11
+ - `host:port:user:pass` (defaults to HTTP)
12
+ - `host:port` (no auth, defaults to HTTP)
13
+
14
+ - **Proxy Rotation Strategies**:
15
+ - **Random**: Pick a random proxy for each request
16
+ - **Round Robin**: Rotate proxies in order
17
+ - **Auto-Switch on Dead**: Automatically try next proxy if current one fails
18
+ - **Stop on Dead**: Stop execution if proxy fails
19
+
20
+ - **Operations**:
21
+ - **Request**: Make HTTP requests through proxies (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
22
+ - **Test Connection**: Test proxy connection and get external IP
23
+
24
+ - **Resilience**:
25
+ - Automatic retry with failover
26
+ - Connection timeout handling
27
+ - Clear error messages for authentication failures
28
+
29
+ ## Installation
30
+
31
+ ### For n8n Cloud or Self-Hosted
32
+
33
+ 1. Install the package:
34
+ ```bash
35
+ npm install n8n-nodes-netproxy
36
+ ```
37
+
38
+ 2. Restart your n8n instance.
39
+
40
+ ### For Local Development
41
+
42
+ 1. Clone this repository:
43
+ ```bash
44
+ git clone <repository-url>
45
+ cd n8n-nodes-netproxy
46
+ ```
47
+
48
+ 2. Install dependencies:
49
+ ```bash
50
+ npm install
51
+ ```
52
+
53
+ 3. Build the node:
54
+ ```bash
55
+ npm run build
56
+ ```
57
+
58
+ 4. Link the package locally:
59
+ ```bash
60
+ npm link
61
+ ```
62
+
63
+ 5. In your n8n installation directory, link the package:
64
+ ```bash
65
+ npm link n8n-nodes-netproxy
66
+ ```
67
+
68
+ 6. Restart n8n to load the node.
69
+
70
+ ## Usage
71
+
72
+ ### Basic Setup
73
+
74
+ 1. Add the **NetProxy HTTP Request** node to your workflow.
75
+
76
+ 2. Choose your **Operation**:
77
+ - **Request**: For making HTTP requests through proxies
78
+ - **Test Connection**: For testing proxy connectivity
79
+
80
+ 3. Configure **Proxy Source**:
81
+ - **Manual Input (List)**: Paste proxies directly in the node
82
+ - **Credentials**: Use stored credentials (more secure)
83
+
84
+ 4. Select **Rotation Strategy** based on your needs.
85
+
86
+ ### Proxy Format Examples
87
+
88
+ ```
89
+ socks5://user:pass@192.168.1.1:1080
90
+ http://user:pass@192.168.1.1:8080
91
+ https://user:pass@192.168.1.1:8443
92
+ 192.168.1.1:8080:user:pass
93
+ 192.168.1.1:8080
94
+ ```
95
+
96
+ ### Example Workflow: Web Scraping
97
+
98
+ 1. Add **NetProxy HTTP Request** node
99
+ 2. Set Operation to **Request**
100
+ 3. Set Method to **GET**
101
+ 4. Enter target URL
102
+ 5. Paste proxy list
103
+ 6. Set Rotation Strategy to **Random** or **Round Robin**
104
+
105
+ ### Example Workflow: Test Proxy
106
+
107
+ 1. Add **NetProxy HTTP Request** node
108
+ 2. Set Operation to **Test Connection**
109
+ 3. Paste proxy list
110
+ 4. Execute to see your external IP
111
+
112
+ ## Configuration
113
+
114
+ ### Request Operation
115
+
116
+ - **URL**: The target URL for the HTTP request
117
+ - **Method**: HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
118
+ - **Headers**: Custom HTTP headers (optional)
119
+ - **Body**: Request body (JSON, Raw, or Form-Data)
120
+ - **Timeout**: Request timeout in seconds (default: 10)
121
+
122
+ ### Rotation Strategies
123
+
124
+ - **Random**: Best for distributing load across proxies
125
+ - **Round Robin**: Best for sequential processing
126
+ - **Auto-Switch on Dead**: Best for reliability when some proxies may be down
127
+ - **Stop on Dead**: Best for strict validation requirements
128
+
129
+ ## Error Handling
130
+
131
+ The node provides clear error messages for:
132
+
133
+ - **407 Proxy Authentication Required**: Invalid proxy credentials
134
+ - **Connection Timeout**: Proxy server not responding
135
+ - **Connection Refused**: Proxy server is down
136
+ - **Dead Proxy**: Proxy failed and failover attempted (if enabled)
137
+
138
+ ## Security
139
+
140
+ - Proxy credentials are never logged in execution output
141
+ - Use **Credentials** option for sensitive proxy lists
142
+ - All inputs are sanitized (trimmed) before processing
143
+
144
+ ## Development
145
+
146
+ ### Building
147
+
148
+ ```bash
149
+ npm run build
150
+ ```
151
+
152
+ ### Linting
153
+
154
+ ```bash
155
+ npm run lint
156
+ ```
157
+
158
+ ### Formatting
159
+
160
+ ```bash
161
+ npm run format
162
+ ```
163
+
164
+ ## License
165
+
166
+ MIT
167
+
168
+ ## Author
169
+
170
+ netproxy.io
171
+
@@ -0,0 +1,7 @@
1
+ import { ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class NetProxyApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ description: string;
6
+ properties: INodeProperties[];
7
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NetProxyApi = void 0;
4
+ class NetProxyApi {
5
+ constructor() {
6
+ this.name = 'netProxyApi';
7
+ this.displayName = 'NetProxy API';
8
+ this.description = 'Store proxy list securely for NetProxy node';
9
+ this.properties = [
10
+ {
11
+ displayName: 'Proxy List',
12
+ name: 'proxyList',
13
+ type: 'string',
14
+ typeOptions: {
15
+ rows: 5,
16
+ },
17
+ default: '',
18
+ description: 'Paste proxies here (one per line). Formats: socks5://user:pass@host:port, http://user:pass@host:port, or host:port:user:pass',
19
+ },
20
+ ];
21
+ }
22
+ }
23
+ exports.NetProxyApi = NetProxyApi;
@@ -0,0 +1,5 @@
1
+ import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class NetProxy implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,541 @@
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 () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.NetProxy = void 0;
37
+ const axios_1 = __importStar(require("axios"));
38
+ const hpagent_1 = require("hpagent");
39
+ const socks_proxy_agent_1 = require("socks-proxy-agent");
40
+ const utils_1 = require("./utils");
41
+ // Standalone helper functions (moved outside class to avoid 'this' context issues)
42
+ function selectProxy(proxies, strategy, staticData) {
43
+ if (proxies.length === 0) {
44
+ throw new Error('No proxies available');
45
+ }
46
+ if (strategy === 'random') {
47
+ return proxies[Math.floor(Math.random() * proxies.length)];
48
+ }
49
+ else if (strategy === 'roundRobin' || strategy === 'failover') {
50
+ const index = staticData.roundRobinIndex % proxies.length;
51
+ staticData.roundRobinIndex = (staticData.roundRobinIndex + 1) % proxies.length;
52
+ return proxies[index];
53
+ }
54
+ else {
55
+ // Default to first proxy
56
+ return proxies[0];
57
+ }
58
+ }
59
+ function createProxyAgent(proxy) {
60
+ const proxyUrl = proxy.auth
61
+ ? `${proxy.protocol}://${proxy.auth.username}:${proxy.auth.password}@${proxy.host}:${proxy.port}`
62
+ : `${proxy.protocol}://${proxy.host}:${proxy.port}`;
63
+ if (proxy.protocol === 'socks5') {
64
+ return new socks_proxy_agent_1.SocksProxyAgent(proxyUrl);
65
+ }
66
+ else if (proxy.protocol === 'https') {
67
+ return new hpagent_1.HttpsProxyAgent({
68
+ proxy: proxyUrl,
69
+ });
70
+ }
71
+ else {
72
+ // HTTP proxy
73
+ return new hpagent_1.HttpProxyAgent({
74
+ proxy: proxyUrl,
75
+ });
76
+ }
77
+ }
78
+ async function testConnection(proxies, rotationStrategy, staticData) {
79
+ const selectedProxy = selectProxy(proxies, rotationStrategy, staticData);
80
+ const agent = createProxyAgent(selectedProxy);
81
+ const testUrl = 'https://api.ipify.org?format=json';
82
+ try {
83
+ const response = await axios_1.default.get(testUrl, {
84
+ httpsAgent: agent,
85
+ httpAgent: agent,
86
+ timeout: 10000,
87
+ });
88
+ return {
89
+ success: true,
90
+ ip: response.data.ip,
91
+ proxy: `${selectedProxy.host}:${selectedProxy.port}`,
92
+ protocol: selectedProxy.protocol,
93
+ };
94
+ }
95
+ catch (error) {
96
+ if (rotationStrategy === 'failover' && proxies.length > 1) {
97
+ // Try next proxy
98
+ const nextProxy = selectProxy(proxies, 'roundRobin', staticData);
99
+ const nextAgent = createProxyAgent(nextProxy);
100
+ try {
101
+ const response = await axios_1.default.get(testUrl, {
102
+ httpsAgent: nextAgent,
103
+ httpAgent: nextAgent,
104
+ timeout: 10000,
105
+ });
106
+ return {
107
+ success: true,
108
+ ip: response.data.ip,
109
+ proxy: `${nextProxy.host}:${nextProxy.port}`,
110
+ protocol: nextProxy.protocol,
111
+ warning: `Original proxy failed, switched to backup`,
112
+ };
113
+ }
114
+ catch (retryError) {
115
+ throw new Error(`All proxies failed. Last error: ${retryError instanceof Error ? retryError.message : String(retryError)}`);
116
+ }
117
+ }
118
+ throw new Error(`Proxy test failed: ${error instanceof Error ? error.message : String(error)}`);
119
+ }
120
+ }
121
+ async function makeRequest(executeFunctions, itemIndex, proxies, rotationStrategy, staticData) {
122
+ const url = executeFunctions.getNodeParameter('url', itemIndex);
123
+ const method = executeFunctions.getNodeParameter('method', itemIndex).toUpperCase();
124
+ const timeout = executeFunctions.getNodeParameter('timeout', itemIndex) * 1000 || 10000;
125
+ const sendHeaders = executeFunctions.getNodeParameter('sendHeaders', itemIndex, false);
126
+ const sendBody = executeFunctions.getNodeParameter('sendBody', itemIndex, false);
127
+ const config = {
128
+ method: method,
129
+ url,
130
+ timeout,
131
+ };
132
+ // Add headers
133
+ if (sendHeaders) {
134
+ const headerParameters = executeFunctions.getNodeParameter('headerParameters', itemIndex, {});
135
+ config.headers = {};
136
+ if (headerParameters.header && Array.isArray(headerParameters.header)) {
137
+ for (const header of headerParameters.header) {
138
+ config.headers[header.name] = header.value;
139
+ }
140
+ }
141
+ }
142
+ // Add body
143
+ if (sendBody) {
144
+ const bodyContentType = executeFunctions.getNodeParameter('bodyContentType', itemIndex);
145
+ const body = executeFunctions.getNodeParameter('body', itemIndex);
146
+ if (bodyContentType === 'json') {
147
+ try {
148
+ config.data = JSON.parse(body);
149
+ config.headers = config.headers || {};
150
+ config.headers['Content-Type'] = 'application/json';
151
+ }
152
+ catch (e) {
153
+ throw new Error('Invalid JSON in body');
154
+ }
155
+ }
156
+ else if (bodyContentType === 'raw') {
157
+ config.data = body;
158
+ }
159
+ else if (bodyContentType === 'formData') {
160
+ // Parse form data (simple key=value format)
161
+ const formData = {};
162
+ const pairs = body.split('&');
163
+ for (const pair of pairs) {
164
+ const [key, value] = pair.split('=');
165
+ if (key && value) {
166
+ formData[decodeURIComponent(key)] = decodeURIComponent(value);
167
+ }
168
+ }
169
+ config.data = formData;
170
+ }
171
+ }
172
+ // Select proxy and create agent
173
+ let selectedProxy = selectProxy(proxies, rotationStrategy, staticData);
174
+ let agent = createProxyAgent(selectedProxy);
175
+ config.httpsAgent = agent;
176
+ config.httpAgent = agent;
177
+ // Make request with failover logic
178
+ const maxRetries = rotationStrategy === 'failover' ? proxies.length : 1;
179
+ let lastError = null;
180
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
181
+ try {
182
+ const response = await (0, axios_1.default)(config);
183
+ return {
184
+ status: response.status,
185
+ statusText: response.statusText,
186
+ headers: response.headers,
187
+ data: response.data,
188
+ proxy: `${selectedProxy.host}:${selectedProxy.port}`,
189
+ protocol: selectedProxy.protocol,
190
+ };
191
+ }
192
+ catch (error) {
193
+ lastError = error;
194
+ // Handle specific error cases
195
+ if (error instanceof axios_1.AxiosError) {
196
+ if (error.response?.status === 407) {
197
+ throw new Error(`Proxy authentication failed for ${selectedProxy.host}:${selectedProxy.port}. Please check credentials.`);
198
+ }
199
+ if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
200
+ if (rotationStrategy === 'failover' && attempt < maxRetries - 1) {
201
+ // Try next proxy
202
+ selectedProxy = selectProxy(proxies, 'roundRobin', staticData);
203
+ agent = createProxyAgent(selectedProxy);
204
+ config.httpsAgent = agent;
205
+ config.httpAgent = agent;
206
+ continue;
207
+ }
208
+ else if (rotationStrategy === 'stopOnDead') {
209
+ throw new Error(`Proxy ${selectedProxy.host}:${selectedProxy.port} is dead. Stopping execution.`);
210
+ }
211
+ }
212
+ }
213
+ // If not failover or last attempt, throw error
214
+ if (rotationStrategy !== 'failover' || attempt === maxRetries - 1) {
215
+ throw new Error(`Request failed: ${error instanceof Error ? error.message : String(error)}`);
216
+ }
217
+ }
218
+ }
219
+ throw lastError || new Error('Request failed after all retries');
220
+ }
221
+ class NetProxy {
222
+ constructor() {
223
+ this.description = {
224
+ displayName: 'NetProxy HTTP Request',
225
+ name: 'netProxy',
226
+ icon: 'file:NetProxy.png',
227
+ group: ['transform'],
228
+ version: 1,
229
+ subtitle: '={{$parameter["operation"]}}',
230
+ description: 'Route HTTP requests through NetProxy.io residential proxies with auto-rotation',
231
+ defaults: {
232
+ name: 'NetProxy HTTP Request',
233
+ },
234
+ inputs: ['main'],
235
+ outputs: ['main'],
236
+ credentials: [
237
+ {
238
+ name: 'netProxyApi',
239
+ required: false,
240
+ },
241
+ ],
242
+ properties: [
243
+ {
244
+ displayName: 'Operation',
245
+ name: 'operation',
246
+ type: 'options',
247
+ noDataExpression: true,
248
+ options: [
249
+ {
250
+ name: 'Request',
251
+ value: 'request',
252
+ description: 'Make an HTTP request through proxy',
253
+ },
254
+ {
255
+ name: 'Test Connection',
256
+ value: 'testConnection',
257
+ description: 'Test proxy connection and get external IP',
258
+ },
259
+ ],
260
+ default: 'request',
261
+ },
262
+ {
263
+ displayName: 'Proxy Source',
264
+ name: 'proxySource',
265
+ type: 'options',
266
+ default: 'manual',
267
+ options: [
268
+ {
269
+ name: 'Manual Input (List)',
270
+ value: 'manual',
271
+ },
272
+ {
273
+ name: 'Credentials',
274
+ value: 'credentials',
275
+ },
276
+ ],
277
+ },
278
+ {
279
+ displayName: 'Proxy List',
280
+ name: 'proxyList',
281
+ type: 'string',
282
+ typeOptions: {
283
+ rows: 5,
284
+ },
285
+ default: '',
286
+ displayOptions: {
287
+ show: {
288
+ proxySource: ['manual'],
289
+ },
290
+ },
291
+ description: 'Paste proxies here (one per line). Formats: socks5://user:pass@host:port, http://user:pass@host:port, or host:port:user:pass',
292
+ },
293
+ {
294
+ displayName: 'Rotation Strategy',
295
+ name: 'rotationStrategy',
296
+ type: 'options',
297
+ default: 'random',
298
+ options: [
299
+ {
300
+ name: 'Random',
301
+ value: 'random',
302
+ description: 'Pick a random proxy for each request',
303
+ },
304
+ {
305
+ name: 'Round Robin',
306
+ value: 'roundRobin',
307
+ description: 'Rotate proxies in order',
308
+ },
309
+ {
310
+ name: 'Auto-Switch on Dead',
311
+ value: 'failover',
312
+ description: 'Try next proxy if current one fails',
313
+ },
314
+ {
315
+ name: 'Stop on Dead',
316
+ value: 'stopOnDead',
317
+ description: 'Stop execution if proxy fails',
318
+ },
319
+ ],
320
+ },
321
+ {
322
+ displayName: 'Request URL',
323
+ name: 'url',
324
+ type: 'string',
325
+ default: '',
326
+ required: true,
327
+ displayOptions: {
328
+ show: {
329
+ operation: ['request'],
330
+ },
331
+ },
332
+ description: 'The URL to make the request to',
333
+ },
334
+ {
335
+ displayName: 'Method',
336
+ name: 'method',
337
+ type: 'options',
338
+ options: [
339
+ {
340
+ name: 'GET',
341
+ value: 'GET',
342
+ },
343
+ {
344
+ name: 'POST',
345
+ value: 'POST',
346
+ },
347
+ {
348
+ name: 'PUT',
349
+ value: 'PUT',
350
+ },
351
+ {
352
+ name: 'PATCH',
353
+ value: 'PATCH',
354
+ },
355
+ {
356
+ name: 'DELETE',
357
+ value: 'DELETE',
358
+ },
359
+ {
360
+ name: 'HEAD',
361
+ value: 'HEAD',
362
+ },
363
+ {
364
+ name: 'OPTIONS',
365
+ value: 'OPTIONS',
366
+ },
367
+ ],
368
+ default: 'GET',
369
+ displayOptions: {
370
+ show: {
371
+ operation: ['request'],
372
+ },
373
+ },
374
+ },
375
+ {
376
+ displayName: 'Send Headers',
377
+ name: 'sendHeaders',
378
+ type: 'boolean',
379
+ default: false,
380
+ displayOptions: {
381
+ show: {
382
+ operation: ['request'],
383
+ },
384
+ },
385
+ },
386
+ {
387
+ displayName: 'Header Parameters',
388
+ name: 'headerParameters',
389
+ type: 'fixedCollection',
390
+ typeOptions: {
391
+ multipleValues: true,
392
+ },
393
+ default: {},
394
+ displayOptions: {
395
+ show: {
396
+ operation: ['request'],
397
+ sendHeaders: [true],
398
+ },
399
+ },
400
+ placeholder: 'Add Header',
401
+ options: [
402
+ {
403
+ displayName: 'Header',
404
+ name: 'header',
405
+ values: [
406
+ {
407
+ displayName: 'Name',
408
+ name: 'name',
409
+ type: 'string',
410
+ default: '',
411
+ },
412
+ {
413
+ displayName: 'Value',
414
+ name: 'value',
415
+ type: 'string',
416
+ default: '',
417
+ },
418
+ ],
419
+ },
420
+ ],
421
+ },
422
+ {
423
+ displayName: 'Send Body',
424
+ name: 'sendBody',
425
+ type: 'boolean',
426
+ default: false,
427
+ displayOptions: {
428
+ show: {
429
+ operation: ['request'],
430
+ },
431
+ },
432
+ },
433
+ {
434
+ displayName: 'Body Content Type',
435
+ name: 'bodyContentType',
436
+ type: 'options',
437
+ options: [
438
+ {
439
+ name: 'JSON',
440
+ value: 'json',
441
+ },
442
+ {
443
+ name: 'Raw',
444
+ value: 'raw',
445
+ },
446
+ {
447
+ name: 'Form-Data',
448
+ value: 'formData',
449
+ },
450
+ ],
451
+ default: 'json',
452
+ displayOptions: {
453
+ show: {
454
+ operation: ['request'],
455
+ sendBody: [true],
456
+ },
457
+ },
458
+ },
459
+ {
460
+ displayName: 'Body',
461
+ name: 'body',
462
+ type: 'string',
463
+ default: '',
464
+ displayOptions: {
465
+ show: {
466
+ operation: ['request'],
467
+ sendBody: [true],
468
+ },
469
+ },
470
+ },
471
+ {
472
+ displayName: 'Timeout (seconds)',
473
+ name: 'timeout',
474
+ type: 'number',
475
+ default: 10,
476
+ displayOptions: {
477
+ show: {
478
+ operation: ['request'],
479
+ },
480
+ },
481
+ description: 'Request timeout in seconds',
482
+ },
483
+ ],
484
+ };
485
+ }
486
+ async execute() {
487
+ const items = this.getInputData();
488
+ const returnData = [];
489
+ const operation = this.getNodeParameter('operation', 0);
490
+ // Get proxy list
491
+ const proxySource = this.getNodeParameter('proxySource', 0);
492
+ let proxyListString = '';
493
+ if (proxySource === 'credentials') {
494
+ const credentials = await this.getCredentials('netProxyApi');
495
+ proxyListString = credentials?.proxyList || '';
496
+ }
497
+ else {
498
+ proxyListString = this.getNodeParameter('proxyList', 0) || '';
499
+ }
500
+ const parsedProxies = (0, utils_1.parseProxyList)(proxyListString);
501
+ if (parsedProxies.length === 0) {
502
+ throw new Error('No valid proxies found. Please provide at least one valid proxy.');
503
+ }
504
+ const rotationStrategy = this.getNodeParameter('rotationStrategy', 0);
505
+ // Get static data for round-robin tracking
506
+ const staticData = this.getWorkflowStaticData('global');
507
+ if (!staticData.roundRobinIndex) {
508
+ staticData.roundRobinIndex = 0;
509
+ }
510
+ for (let i = 0; i < items.length; i++) {
511
+ try {
512
+ if (operation === 'testConnection') {
513
+ const result = await testConnection(parsedProxies, rotationStrategy, staticData);
514
+ returnData.push({
515
+ json: result,
516
+ });
517
+ }
518
+ else if (operation === 'request') {
519
+ const result = await makeRequest(this, i, parsedProxies, rotationStrategy, staticData);
520
+ returnData.push({
521
+ json: result,
522
+ });
523
+ }
524
+ }
525
+ catch (error) {
526
+ if (this.continueOnFail()) {
527
+ returnData.push({
528
+ json: {
529
+ error: error instanceof Error ? error.message : String(error),
530
+ success: false,
531
+ },
532
+ });
533
+ continue;
534
+ }
535
+ throw error;
536
+ }
537
+ }
538
+ return [returnData];
539
+ }
540
+ }
541
+ exports.NetProxy = NetProxy;
@@ -0,0 +1 @@
1
+ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Utility functions for parsing and validating proxy strings
3
+ */
4
+ export interface ParsedProxy {
5
+ host: string;
6
+ port: number;
7
+ auth?: {
8
+ username: string;
9
+ password: string;
10
+ };
11
+ protocol: 'http' | 'https' | 'socks5';
12
+ }
13
+ /**
14
+ * Parses a proxy string in various formats into a standardized object
15
+ * Supported formats:
16
+ * 1. socks5://user:pass@host:port
17
+ * 2. http://user:pass@host:port
18
+ * 3. https://user:pass@host:port
19
+ * 4. host:port:user:pass (defaults to http)
20
+ * 5. host:port (no auth, defaults to http)
21
+ *
22
+ * @param proxyString - The proxy string to parse
23
+ * @returns ParsedProxy object or null if parsing fails
24
+ */
25
+ export declare function parseProxyString(proxyString: string): ParsedProxy | null;
26
+ /**
27
+ * Parses multiple proxy strings from a multiline string
28
+ * @param proxyListString - Multiline string containing proxy strings (one per line)
29
+ * @returns Array of ParsedProxy objects (invalid entries are filtered out)
30
+ */
31
+ export declare function parseProxyList(proxyListString: string): ParsedProxy[];
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ /**
3
+ * Utility functions for parsing and validating proxy strings
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseProxyString = parseProxyString;
7
+ exports.parseProxyList = parseProxyList;
8
+ /**
9
+ * Parses a proxy string in various formats into a standardized object
10
+ * Supported formats:
11
+ * 1. socks5://user:pass@host:port
12
+ * 2. http://user:pass@host:port
13
+ * 3. https://user:pass@host:port
14
+ * 4. host:port:user:pass (defaults to http)
15
+ * 5. host:port (no auth, defaults to http)
16
+ *
17
+ * @param proxyString - The proxy string to parse
18
+ * @returns ParsedProxy object or null if parsing fails
19
+ */
20
+ function parseProxyString(proxyString) {
21
+ if (!proxyString || typeof proxyString !== 'string') {
22
+ return null;
23
+ }
24
+ // Trim whitespace
25
+ const trimmed = proxyString.trim();
26
+ if (!trimmed) {
27
+ return null;
28
+ }
29
+ // Format 1 & 2: Protocol-prefixed with auth (socks5://user:pass@host:port or http://user:pass@host:port)
30
+ const protocolWithAuthRegex = /^(socks5|http|https):\/\/(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$/;
31
+ const matchProtocolWithAuth = trimmed.match(protocolWithAuthRegex);
32
+ if (matchProtocolWithAuth) {
33
+ const [, protocol, username, password, host, port] = matchProtocolWithAuth;
34
+ const parsed = {
35
+ host: host.trim(),
36
+ port: parseInt(port, 10),
37
+ protocol: protocol === 'socks5' ? 'socks5' : protocol === 'https' ? 'https' : 'http',
38
+ };
39
+ if (username && password) {
40
+ parsed.auth = {
41
+ username: username.trim(),
42
+ password: password.trim(),
43
+ };
44
+ }
45
+ if (isValidProxy(parsed)) {
46
+ return parsed;
47
+ }
48
+ }
49
+ // Format 3: host:port:user:pass (no protocol, defaults to http)
50
+ const hostPortUserPassRegex = /^([^:]+):(\d+):([^:]+):(.+)$/;
51
+ const matchHostPortUserPass = trimmed.match(hostPortUserPassRegex);
52
+ if (matchHostPortUserPass) {
53
+ const [, host, port, username, password] = matchHostPortUserPass;
54
+ const parsed = {
55
+ host: host.trim(),
56
+ port: parseInt(port, 10),
57
+ protocol: 'http',
58
+ auth: {
59
+ username: username.trim(),
60
+ password: password.trim(),
61
+ },
62
+ };
63
+ if (isValidProxy(parsed)) {
64
+ return parsed;
65
+ }
66
+ }
67
+ // Format 4: host:port (no auth, no protocol, defaults to http)
68
+ const hostPortRegex = /^([^:]+):(\d+)$/;
69
+ const matchHostPort = trimmed.match(hostPortRegex);
70
+ if (matchHostPort) {
71
+ const [, host, port] = matchHostPort;
72
+ const parsed = {
73
+ host: host.trim(),
74
+ port: parseInt(port, 10),
75
+ protocol: 'http',
76
+ };
77
+ if (isValidProxy(parsed)) {
78
+ return parsed;
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+ /**
84
+ * Validates a parsed proxy object
85
+ * @param proxy - The parsed proxy object to validate
86
+ * @returns true if valid, false otherwise
87
+ */
88
+ function isValidProxy(proxy) {
89
+ if (!proxy.host || !proxy.port) {
90
+ return false;
91
+ }
92
+ if (proxy.port < 1 || proxy.port > 65535) {
93
+ return false;
94
+ }
95
+ if (proxy.auth && (!proxy.auth.username || !proxy.auth.password)) {
96
+ return false;
97
+ }
98
+ return true;
99
+ }
100
+ /**
101
+ * Parses multiple proxy strings from a multiline string
102
+ * @param proxyListString - Multiline string containing proxy strings (one per line)
103
+ * @returns Array of ParsedProxy objects (invalid entries are filtered out)
104
+ */
105
+ function parseProxyList(proxyListString) {
106
+ if (!proxyListString || typeof proxyListString !== 'string') {
107
+ return [];
108
+ }
109
+ const lines = proxyListString
110
+ .split('\n')
111
+ .map((line) => line.trim())
112
+ .filter((line) => line.length > 0);
113
+ const parsedProxies = [];
114
+ for (const line of lines) {
115
+ const parsed = parseProxyString(line);
116
+ if (parsed) {
117
+ parsedProxies.push(parsed);
118
+ }
119
+ }
120
+ return parsedProxies;
121
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@pmtuan0206/n8n-nodes-netproxy-cur",
3
+ "version": "0.1.0",
4
+ "description": "Route HTTP requests through NetProxy.io residential proxies with auto-rotation and failover",
5
+ "keywords": [
6
+ "n8n-community-node",
7
+ "netproxy",
8
+ "proxy",
9
+ "socks5",
10
+ "http-proxy",
11
+ "proxy-rotation"
12
+ ],
13
+ "license": "MIT",
14
+ "homepage": "https://netproxy.io",
15
+ "author": {
16
+ "name": "netproxy.io"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": ""
21
+ },
22
+ "main": "index.js",
23
+ "scripts": {
24
+ "build": "tsc && gulp build:icons",
25
+ "dev": "tsc --watch",
26
+ "format": "prettier nodes credentials --write",
27
+ "lint": "eslint \"nodes/**/*.ts\" \"credentials/**/*.ts\" package.json",
28
+ "lintfix": "eslint \"nodes/**/*.ts\" \"credentials/**/*.ts\" package.json --fix",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "n8n": {
35
+ "n8nNodesApiVersion": 1,
36
+ "nodes": [
37
+ "dist/nodes/NetProxy/NetProxy.node.js"
38
+ ],
39
+ "credentials": [
40
+ "dist/credentials/NetProxyApi.credentials.js"
41
+ ]
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^18.15.0",
45
+ "@typescript-eslint/parser": "^5.57.0",
46
+ "eslint-plugin-n8n-nodes-base": "^1.11.0",
47
+ "gulp": "^4.0.2",
48
+ "n8n-workflow": "*",
49
+ "prettier": "^2.7.1",
50
+ "typescript": "^5.3.0"
51
+ },
52
+ "dependencies": {
53
+ "axios": "^1.6.0",
54
+ "hpagent": "^1.2.0",
55
+ "n8n-nodes-base": "*",
56
+ "socks-proxy-agent": "^8.0.2"
57
+ }
58
+ }
59
+