@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 +171 -0
- package/dist/credentials/NetProxyApi.credentials.d.ts +7 -0
- package/dist/credentials/NetProxyApi.credentials.js +23 -0
- package/dist/nodes/NetProxy/NetProxy.node.d.ts +5 -0
- package/dist/nodes/NetProxy/NetProxy.node.js +541 -0
- package/dist/nodes/NetProxy/NetProxy.png +1 -0
- package/dist/nodes/NetProxy/utils.d.ts +31 -0
- package/dist/nodes/NetProxy/utils.js +121 -0
- package/package.json +59 -0
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,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,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
|
+
|