n8n-nodes-dominusnode 1.2.0 → 1.5.2
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/CHANGELOG.md +1 -0
- package/README.md +13 -10
- package/dist/credentials/DominusNodeApi.credentials.js +1 -1
- package/dist/nodes/DominusNodeAccount/DominusNodeAccount.node.js +132 -9
- package/dist/nodes/DominusNodeAccount/DominusNodeAccount.node.js.map +1 -1
- package/dist/nodes/DominusNodeKeys/DominusNodeKeys.node.js +21 -4
- package/dist/nodes/DominusNodeKeys/DominusNodeKeys.node.js.map +1 -1
- package/dist/nodes/DominusNodePlans/DominusNodePlans.node.js +24 -5
- package/dist/nodes/DominusNodePlans/DominusNodePlans.node.js.map +1 -1
- package/dist/nodes/DominusNodeProxy/DominusNodeProxy.node.js +21 -8
- package/dist/nodes/DominusNodeProxy/DominusNodeProxy.node.js.map +1 -1
- package/dist/nodes/DominusNodeTeams/DominusNodeTeams.node.js +63 -12
- package/dist/nodes/DominusNodeTeams/DominusNodeTeams.node.js.map +1 -1
- package/dist/nodes/DominusNodeUsage/DominusNodeUsage.node.js.map +1 -1
- package/dist/nodes/DominusNodeWallet/DominusNodeWallet.node.js +320 -40
- package/dist/nodes/DominusNodeWallet/DominusNodeWallet.node.js.map +1 -1
- package/dist/shared/auth.js +1 -1
- package/dist/shared/auth.js.map +1 -1
- package/dist/shared/ssrf.js.map +1 -1
- package/package.json +2 -1
- package/src/credentials/DominusNodeApi.credentials.ts +0 -48
- package/src/index.ts +0 -8
- package/src/nodes/DominusNodeAccount/DominusNodeAccount.node.ts +0 -283
- package/src/nodes/DominusNodeKeys/DominusNodeKeys.node.ts +0 -192
- package/src/nodes/DominusNodePlans/DominusNodePlans.node.ts +0 -154
- package/src/nodes/DominusNodeProxy/DominusNodeProxy.node.ts +0 -470
- package/src/nodes/DominusNodeTeams/DominusNodeTeams.node.ts +0 -351
- package/src/nodes/DominusNodeUsage/DominusNodeUsage.node.ts +0 -183
- package/src/nodes/DominusNodeWallet/DominusNodeWallet.node.ts +0 -950
- package/src/shared/auth.ts +0 -287
- package/src/shared/constants.ts +0 -11
- package/src/shared/ssrf.ts +0 -257
- package/tests/DominusNodeProxy.test.ts +0 -281
- package/tests/DominusNodeUsage.test.ts +0 -250
- package/tests/DominusNodeWallet.test.ts +0 -591
- package/tests/ssrf.test.ts +0 -238
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -8
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dominus Node Plans n8n community node.
|
|
3
|
-
*
|
|
4
|
-
* Operations (3 tools):
|
|
5
|
-
* - Get Plan: Get current user plan
|
|
6
|
-
* - List Plans: List all available plans
|
|
7
|
-
* - Change Plan: Switch to a different plan
|
|
8
|
-
*
|
|
9
|
-
* Security:
|
|
10
|
-
* - UUID validation for plan IDs
|
|
11
|
-
* - Credential sanitization in error messages
|
|
12
|
-
*
|
|
13
|
-
* @module
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
IDataObject,
|
|
18
|
-
IExecuteFunctions,
|
|
19
|
-
INodeExecutionData,
|
|
20
|
-
INodeType,
|
|
21
|
-
INodeTypeDescription,
|
|
22
|
-
NodeOperationError,
|
|
23
|
-
} from "n8n-workflow";
|
|
24
|
-
|
|
25
|
-
import { DominusNodeAuth, sanitizeError } from "../../shared/auth";
|
|
26
|
-
|
|
27
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
28
|
-
|
|
29
|
-
export class DominusNodePlans implements INodeType {
|
|
30
|
-
description: INodeTypeDescription = {
|
|
31
|
-
displayName: "Dominus Node Plans",
|
|
32
|
-
name: "dominusNodePlans",
|
|
33
|
-
icon: "file:dominusnode.svg",
|
|
34
|
-
group: ["transform"],
|
|
35
|
-
version: 1,
|
|
36
|
-
subtitle: '={{$parameter["operation"]}}',
|
|
37
|
-
description: "Manage Dominus Node subscription plans",
|
|
38
|
-
defaults: { name: "Dominus Node Plans" },
|
|
39
|
-
inputs: ["main"],
|
|
40
|
-
outputs: ["main"],
|
|
41
|
-
credentials: [{ name: "dominusNodeApi", required: true }],
|
|
42
|
-
properties: [
|
|
43
|
-
{
|
|
44
|
-
displayName: "Operation",
|
|
45
|
-
name: "operation",
|
|
46
|
-
type: "options",
|
|
47
|
-
noDataExpression: true,
|
|
48
|
-
options: [
|
|
49
|
-
{ name: "Get Plan", value: "getPlan", description: "Get current user plan", action: "Get plan" },
|
|
50
|
-
{ name: "List Plans", value: "listPlans", description: "List all available plans", action: "List plans" },
|
|
51
|
-
{ name: "Change Plan", value: "changePlan", description: "Switch to a different plan", action: "Change plan" },
|
|
52
|
-
],
|
|
53
|
-
default: "getPlan",
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
// --- Change Plan ---
|
|
57
|
-
{
|
|
58
|
-
displayName: "Plan ID",
|
|
59
|
-
name: "planId",
|
|
60
|
-
type: "string",
|
|
61
|
-
default: "",
|
|
62
|
-
required: true,
|
|
63
|
-
description: "UUID of the plan to switch to",
|
|
64
|
-
displayOptions: { show: { operation: ["changePlan"] } },
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
70
|
-
const items = this.getInputData();
|
|
71
|
-
const returnData: INodeExecutionData[] = [];
|
|
72
|
-
const credentials = await this.getCredentials("dominusNodeApi");
|
|
73
|
-
|
|
74
|
-
const apiKey = credentials.apiKey as string;
|
|
75
|
-
const baseUrl = (credentials.baseUrl as string) || "https://api.dominusnode.com";
|
|
76
|
-
|
|
77
|
-
if (!apiKey) {
|
|
78
|
-
throw new NodeOperationError(this.getNode(), "API Key is required");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const agentSecret = (credentials.agentSecret as string) || undefined;
|
|
82
|
-
const auth = new DominusNodeAuth(apiKey, baseUrl, 30000, agentSecret);
|
|
83
|
-
const operation = this.getNodeParameter("operation", 0) as string;
|
|
84
|
-
|
|
85
|
-
for (let i = 0; i < items.length; i++) {
|
|
86
|
-
try {
|
|
87
|
-
let result: unknown;
|
|
88
|
-
|
|
89
|
-
switch (operation) {
|
|
90
|
-
case "getPlan": {
|
|
91
|
-
result = await auth.apiRequest("GET", "/api/plans/user/plan");
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
case "listPlans": {
|
|
96
|
-
result = await auth.apiRequest("GET", "/api/plans");
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
case "changePlan": {
|
|
101
|
-
const planId = this.getNodeParameter("planId", i) as string;
|
|
102
|
-
validateUuid(this, planId, "planId", i);
|
|
103
|
-
result = await auth.apiRequest("PUT", "/api/plans/user/plan", { planId });
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
default:
|
|
108
|
-
throw new NodeOperationError(
|
|
109
|
-
this.getNode(),
|
|
110
|
-
`Unknown operation: ${operation}`,
|
|
111
|
-
{ itemIndex: i },
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
returnData.push({ json: (result ?? {}) as IDataObject });
|
|
116
|
-
} catch (err) {
|
|
117
|
-
if (this.continueOnFail()) {
|
|
118
|
-
returnData.push({
|
|
119
|
-
json: {
|
|
120
|
-
error: sanitizeError(err instanceof Error ? err.message : String(err)),
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
if (err instanceof NodeOperationError) throw err;
|
|
126
|
-
throw new NodeOperationError(
|
|
127
|
-
this.getNode(),
|
|
128
|
-
sanitizeError(err instanceof Error ? err.message : String(err)),
|
|
129
|
-
{ itemIndex: i },
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return [returnData];
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
// Validation helpers
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
function validateUuid(
|
|
143
|
-
ctx: IExecuteFunctions,
|
|
144
|
-
value: string,
|
|
145
|
-
fieldName: string,
|
|
146
|
-
itemIndex: number,
|
|
147
|
-
): void {
|
|
148
|
-
if (!value || typeof value !== "string") {
|
|
149
|
-
throw new NodeOperationError(ctx.getNode(), `${fieldName} is required`, { itemIndex });
|
|
150
|
-
}
|
|
151
|
-
if (!UUID_RE.test(value)) {
|
|
152
|
-
throw new NodeOperationError(ctx.getNode(), `${fieldName} must be a valid UUID`, { itemIndex });
|
|
153
|
-
}
|
|
154
|
-
}
|
|
@@ -1,470 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dominus Node Proxy n8n community node.
|
|
3
|
-
*
|
|
4
|
-
* Operations (4 tools):
|
|
5
|
-
* - Proxied Fetch: Make HTTP requests through Dominus Node's rotating proxy network
|
|
6
|
-
* - Get Proxy Config: Retrieve proxy endpoint configuration
|
|
7
|
-
* - List Active Sessions: List currently active proxy sessions
|
|
8
|
-
* - Get Proxy Status: Get proxy pool health and availability status
|
|
9
|
-
*
|
|
10
|
-
* Security:
|
|
11
|
-
* - Full SSRF prevention (private IPs, hex/octal/decimal, IPv6 variants,
|
|
12
|
-
* Teredo, 6to4, CGNAT, multicast, .localhost/.local/.internal/.arpa)
|
|
13
|
-
* - DNS rebinding protection
|
|
14
|
-
* - OFAC sanctioned country blocking
|
|
15
|
-
* - Read-only HTTP methods only (GET, HEAD, OPTIONS)
|
|
16
|
-
* - Credential sanitization in error messages
|
|
17
|
-
* - Prototype pollution prevention
|
|
18
|
-
*
|
|
19
|
-
* @module
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import * as http from "node:http";
|
|
23
|
-
import * as tls from "node:tls";
|
|
24
|
-
import {
|
|
25
|
-
IDataObject,
|
|
26
|
-
IExecuteFunctions,
|
|
27
|
-
INodeExecutionData,
|
|
28
|
-
INodeType,
|
|
29
|
-
INodeTypeDescription,
|
|
30
|
-
NodeOperationError,
|
|
31
|
-
} from "n8n-workflow";
|
|
32
|
-
|
|
33
|
-
import { validateUrl } from "../../shared/ssrf";
|
|
34
|
-
import {
|
|
35
|
-
DominusNodeAuth,
|
|
36
|
-
sanitizeError,
|
|
37
|
-
checkDnsRebinding,
|
|
38
|
-
SANCTIONED_COUNTRIES,
|
|
39
|
-
} from "../../shared/auth";
|
|
40
|
-
import { ALLOWED_METHODS, MAX_BODY_TRUNCATE } from "../../shared/constants";
|
|
41
|
-
|
|
42
|
-
const BLOCKED_HEADERS = new Set([
|
|
43
|
-
"host",
|
|
44
|
-
"connection",
|
|
45
|
-
"content-length",
|
|
46
|
-
"transfer-encoding",
|
|
47
|
-
"proxy-authorization",
|
|
48
|
-
"authorization",
|
|
49
|
-
"user-agent",
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
export class DominusNodeProxy implements INodeType {
|
|
53
|
-
description: INodeTypeDescription = {
|
|
54
|
-
displayName: "Dominus Node Proxy",
|
|
55
|
-
name: "dominusNodeProxy",
|
|
56
|
-
icon: "file:dominusnode.svg",
|
|
57
|
-
group: ["transform"],
|
|
58
|
-
version: 1,
|
|
59
|
-
subtitle: '={{$parameter["operation"]}}',
|
|
60
|
-
description: "Make requests through Dominus Node rotating proxy network",
|
|
61
|
-
defaults: { name: "Dominus Node Proxy" },
|
|
62
|
-
inputs: ["main"],
|
|
63
|
-
outputs: ["main"],
|
|
64
|
-
credentials: [{ name: "dominusNodeApi", required: true }],
|
|
65
|
-
properties: [
|
|
66
|
-
{
|
|
67
|
-
displayName: "Operation",
|
|
68
|
-
name: "operation",
|
|
69
|
-
type: "options",
|
|
70
|
-
noDataExpression: true,
|
|
71
|
-
options: [
|
|
72
|
-
{
|
|
73
|
-
name: "Proxied Fetch",
|
|
74
|
-
value: "proxiedFetch",
|
|
75
|
-
description: "Make an HTTP request through the proxy network",
|
|
76
|
-
action: "Proxied fetch",
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
name: "Get Proxy Config",
|
|
80
|
-
value: "getProxyConfig",
|
|
81
|
-
description: "Get proxy endpoint configuration",
|
|
82
|
-
action: "Get proxy config",
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
name: "List Active Sessions",
|
|
86
|
-
value: "listActiveSessions",
|
|
87
|
-
description: "List currently active proxy sessions",
|
|
88
|
-
action: "List active sessions",
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
name: "Get Proxy Status",
|
|
92
|
-
value: "getProxyStatus",
|
|
93
|
-
description: "Get proxy pool health and availability status",
|
|
94
|
-
action: "Get proxy status",
|
|
95
|
-
},
|
|
96
|
-
],
|
|
97
|
-
default: "proxiedFetch",
|
|
98
|
-
},
|
|
99
|
-
// Proxied Fetch parameters
|
|
100
|
-
{
|
|
101
|
-
displayName: "URL",
|
|
102
|
-
name: "url",
|
|
103
|
-
type: "string",
|
|
104
|
-
default: "",
|
|
105
|
-
required: true,
|
|
106
|
-
description: "The URL to fetch through the proxy",
|
|
107
|
-
displayOptions: { show: { operation: ["proxiedFetch"] } },
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
displayName: "Method",
|
|
111
|
-
name: "method",
|
|
112
|
-
type: "options",
|
|
113
|
-
options: [
|
|
114
|
-
{ name: "GET", value: "GET" },
|
|
115
|
-
{ name: "HEAD", value: "HEAD" },
|
|
116
|
-
{ name: "OPTIONS", value: "OPTIONS" },
|
|
117
|
-
],
|
|
118
|
-
default: "GET",
|
|
119
|
-
description: "HTTP method (only read-only methods allowed)",
|
|
120
|
-
displayOptions: { show: { operation: ["proxiedFetch"] } },
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
displayName: "Proxy Type",
|
|
124
|
-
name: "proxyType",
|
|
125
|
-
type: "options",
|
|
126
|
-
options: [
|
|
127
|
-
{ name: "Datacenter ($3/GB)", value: "dc" },
|
|
128
|
-
{ name: "Residential ($5/GB)", value: "residential" },
|
|
129
|
-
{ name: "Auto", value: "auto" },
|
|
130
|
-
],
|
|
131
|
-
default: "dc",
|
|
132
|
-
description: "Type of proxy IP to use",
|
|
133
|
-
displayOptions: { show: { operation: ["proxiedFetch"] } },
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
displayName: "Country",
|
|
137
|
-
name: "country",
|
|
138
|
-
type: "string",
|
|
139
|
-
default: "",
|
|
140
|
-
description: "Two-letter country code for geo-targeting (e.g., US, GB, DE). Leave empty for any.",
|
|
141
|
-
displayOptions: { show: { operation: ["proxiedFetch"] } },
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
displayName: "Custom Headers",
|
|
145
|
-
name: "headers",
|
|
146
|
-
type: "fixedCollection",
|
|
147
|
-
typeOptions: { multipleValues: true },
|
|
148
|
-
default: {},
|
|
149
|
-
description: "Custom HTTP headers to include in the request",
|
|
150
|
-
displayOptions: { show: { operation: ["proxiedFetch"] } },
|
|
151
|
-
options: [
|
|
152
|
-
{
|
|
153
|
-
name: "header",
|
|
154
|
-
displayName: "Header",
|
|
155
|
-
values: [
|
|
156
|
-
{
|
|
157
|
-
displayName: "Name",
|
|
158
|
-
name: "name",
|
|
159
|
-
type: "string",
|
|
160
|
-
default: "",
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
displayName: "Value",
|
|
164
|
-
name: "value",
|
|
165
|
-
type: "string",
|
|
166
|
-
default: "",
|
|
167
|
-
},
|
|
168
|
-
],
|
|
169
|
-
},
|
|
170
|
-
],
|
|
171
|
-
},
|
|
172
|
-
],
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
176
|
-
const items = this.getInputData();
|
|
177
|
-
const returnData: INodeExecutionData[] = [];
|
|
178
|
-
const credentials = await this.getCredentials("dominusNodeApi");
|
|
179
|
-
|
|
180
|
-
const apiKey = credentials.apiKey as string;
|
|
181
|
-
const baseUrl = (credentials.baseUrl as string) || "https://api.dominusnode.com";
|
|
182
|
-
const proxyHost = (credentials.proxyHost as string) || "proxy.dominusnode.com";
|
|
183
|
-
const proxyPort = Number(credentials.proxyPort) || 8080;
|
|
184
|
-
|
|
185
|
-
if (!apiKey) {
|
|
186
|
-
throw new NodeOperationError(this.getNode(), "API Key is required");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const agentSecret = (credentials.agentSecret as string) || undefined;
|
|
190
|
-
const auth = new DominusNodeAuth(apiKey, baseUrl, 30000, agentSecret);
|
|
191
|
-
const operation = this.getNodeParameter("operation", 0) as string;
|
|
192
|
-
|
|
193
|
-
for (let i = 0; i < items.length; i++) {
|
|
194
|
-
try {
|
|
195
|
-
if (operation === "proxiedFetch") {
|
|
196
|
-
const url = this.getNodeParameter("url", i) as string;
|
|
197
|
-
const method = this.getNodeParameter("method", i, "GET") as string;
|
|
198
|
-
const proxyType = this.getNodeParameter("proxyType", i, "dc") as string;
|
|
199
|
-
const country = this.getNodeParameter("country", i, "") as string;
|
|
200
|
-
const headersParam = this.getNodeParameter("headers", i, {}) as {
|
|
201
|
-
header?: Array<{ name: string; value: string }>;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
// Validate URL (SSRF prevention)
|
|
205
|
-
if (!url || typeof url !== "string") {
|
|
206
|
-
throw new NodeOperationError(this.getNode(), "URL is required", { itemIndex: i });
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
let parsedUrl: URL;
|
|
210
|
-
try {
|
|
211
|
-
parsedUrl = validateUrl(url);
|
|
212
|
-
} catch (err) {
|
|
213
|
-
throw new NodeOperationError(
|
|
214
|
-
this.getNode(),
|
|
215
|
-
err instanceof Error ? err.message : "URL validation failed",
|
|
216
|
-
{ itemIndex: i },
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// DNS rebinding protection
|
|
221
|
-
try {
|
|
222
|
-
await checkDnsRebinding(parsedUrl.hostname);
|
|
223
|
-
} catch (err) {
|
|
224
|
-
throw new NodeOperationError(
|
|
225
|
-
this.getNode(),
|
|
226
|
-
err instanceof Error ? err.message : "DNS validation failed",
|
|
227
|
-
{ itemIndex: i },
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Validate method
|
|
232
|
-
const upperMethod = method.toUpperCase();
|
|
233
|
-
if (!ALLOWED_METHODS.has(upperMethod)) {
|
|
234
|
-
throw new NodeOperationError(
|
|
235
|
-
this.getNode(),
|
|
236
|
-
`HTTP method '${upperMethod}' is not allowed. Only GET, HEAD, OPTIONS are permitted.`,
|
|
237
|
-
{ itemIndex: i },
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// OFAC sanctioned country check
|
|
242
|
-
if (country) {
|
|
243
|
-
const upper = country.toUpperCase();
|
|
244
|
-
if (SANCTIONED_COUNTRIES.has(upper)) {
|
|
245
|
-
throw new NodeOperationError(
|
|
246
|
-
this.getNode(),
|
|
247
|
-
`Country '${upper}' is blocked (OFAC sanctioned country)`,
|
|
248
|
-
{ itemIndex: i },
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Build proxy username for geo-targeting (uses hyphens, not underscores)
|
|
254
|
-
const userParts: string[] = [];
|
|
255
|
-
if (proxyType && proxyType !== "auto") userParts.push(proxyType);
|
|
256
|
-
if (country) userParts.push(`country-${country.toUpperCase()}`);
|
|
257
|
-
const username = userParts.length > 0 ? userParts.join("-") : "auto";
|
|
258
|
-
const proxyAuth = "Basic " + Buffer.from(`${username}:${apiKey}`).toString("base64");
|
|
259
|
-
|
|
260
|
-
// Build safe headers
|
|
261
|
-
const safeHeaders: Record<string, string> = {};
|
|
262
|
-
if (headersParam.header) {
|
|
263
|
-
for (const { name, value } of headersParam.header) {
|
|
264
|
-
if (!name) continue;
|
|
265
|
-
if (BLOCKED_HEADERS.has(name.toLowerCase())) continue;
|
|
266
|
-
// CRLF injection prevention
|
|
267
|
-
if (/[\r\n\0]/.test(name) || /[\r\n\0]/.test(value)) continue;
|
|
268
|
-
safeHeaders[name] = value;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Route through proxy gateway
|
|
273
|
-
const MAX_BODY_BYTES = 1_048_576; // 1MB response cap
|
|
274
|
-
const result = await new Promise<{
|
|
275
|
-
status: number;
|
|
276
|
-
headers: Record<string, string>;
|
|
277
|
-
body: string;
|
|
278
|
-
}>((resolve, reject) => {
|
|
279
|
-
const timeout = setTimeout(
|
|
280
|
-
() => reject(new Error("Proxy request timed out after 30000ms")),
|
|
281
|
-
30_000,
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
if (parsedUrl.protocol === "https:") {
|
|
285
|
-
// HTTPS: CONNECT tunnel + TLS
|
|
286
|
-
const connectHost = parsedUrl.hostname.includes(":") ? `[${parsedUrl.hostname}]` : parsedUrl.hostname;
|
|
287
|
-
const connectReq = http.request({
|
|
288
|
-
hostname: proxyHost,
|
|
289
|
-
port: proxyPort,
|
|
290
|
-
method: "CONNECT",
|
|
291
|
-
path: `${connectHost}:${parsedUrl.port || 443}`,
|
|
292
|
-
headers: {
|
|
293
|
-
"Proxy-Authorization": proxyAuth,
|
|
294
|
-
Host: `${connectHost}:${parsedUrl.port || 443}`,
|
|
295
|
-
},
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
connectReq.on("connect", (_res, tunnelSocket) => {
|
|
299
|
-
if (_res.statusCode !== 200) {
|
|
300
|
-
clearTimeout(timeout);
|
|
301
|
-
tunnelSocket.destroy();
|
|
302
|
-
reject(new Error(`CONNECT failed: ${_res.statusCode}`));
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const tlsSocket = tls.connect(
|
|
307
|
-
{
|
|
308
|
-
host: parsedUrl.hostname,
|
|
309
|
-
socket: tunnelSocket,
|
|
310
|
-
servername: parsedUrl.hostname,
|
|
311
|
-
minVersion: "TLSv1.2",
|
|
312
|
-
},
|
|
313
|
-
() => {
|
|
314
|
-
const reqPath = parsedUrl.pathname + parsedUrl.search;
|
|
315
|
-
let reqLine = `${upperMethod} ${reqPath} HTTP/1.1\r\nHost: ${parsedUrl.host}\r\nUser-Agent: n8n-nodes-dominusnode/1.0.0\r\nAccept: */*\r\nConnection: close\r\n`;
|
|
316
|
-
for (const [k, v] of Object.entries(safeHeaders)) {
|
|
317
|
-
if (!["host", "user-agent", "connection"].includes(k.toLowerCase())) {
|
|
318
|
-
reqLine += `${k}: ${v}\r\n`;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
reqLine += "\r\n";
|
|
322
|
-
tlsSocket.write(reqLine);
|
|
323
|
-
|
|
324
|
-
const chunks: Buffer[] = [];
|
|
325
|
-
let byteCount = 0;
|
|
326
|
-
tlsSocket.on("data", (chunk: Buffer) => {
|
|
327
|
-
byteCount += chunk.length;
|
|
328
|
-
if (byteCount <= MAX_BODY_BYTES + 16384) chunks.push(chunk);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
let finalized = false;
|
|
332
|
-
const finalize = () => {
|
|
333
|
-
if (finalized) return;
|
|
334
|
-
finalized = true;
|
|
335
|
-
clearTimeout(timeout);
|
|
336
|
-
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
337
|
-
const headerEnd = raw.indexOf("\r\n\r\n");
|
|
338
|
-
if (headerEnd === -1) {
|
|
339
|
-
reject(new Error("Malformed response"));
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
const headerSection = raw.substring(0, headerEnd);
|
|
343
|
-
const body = raw.substring(headerEnd + 4).substring(0, MAX_BODY_BYTES);
|
|
344
|
-
const statusLine = headerSection.split("\r\n")[0];
|
|
345
|
-
const statusMatch = statusLine.match(/^HTTP\/\d\.\d\s+(\d+)/);
|
|
346
|
-
const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;
|
|
347
|
-
const respHeaders: Record<string, string> = {};
|
|
348
|
-
for (const line of headerSection.split("\r\n").slice(1)) {
|
|
349
|
-
const ci = line.indexOf(":");
|
|
350
|
-
if (ci > 0) {
|
|
351
|
-
respHeaders[line.substring(0, ci).trim().toLowerCase()] =
|
|
352
|
-
line.substring(ci + 1).trim();
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
resolve({ status, headers: respHeaders, body });
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
tlsSocket.on("end", finalize);
|
|
359
|
-
tlsSocket.on("close", finalize);
|
|
360
|
-
tlsSocket.on("error", (err) => {
|
|
361
|
-
clearTimeout(timeout);
|
|
362
|
-
reject(err);
|
|
363
|
-
});
|
|
364
|
-
},
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
tlsSocket.on("error", (err) => {
|
|
368
|
-
clearTimeout(timeout);
|
|
369
|
-
reject(err);
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
connectReq.on("error", (err) => {
|
|
374
|
-
clearTimeout(timeout);
|
|
375
|
-
reject(err);
|
|
376
|
-
});
|
|
377
|
-
connectReq.end();
|
|
378
|
-
} else {
|
|
379
|
-
// HTTP: direct proxy request (full-URL path)
|
|
380
|
-
const req = http.request(
|
|
381
|
-
{
|
|
382
|
-
hostname: proxyHost,
|
|
383
|
-
port: proxyPort,
|
|
384
|
-
method: upperMethod,
|
|
385
|
-
path: url,
|
|
386
|
-
headers: {
|
|
387
|
-
...safeHeaders,
|
|
388
|
-
"Proxy-Authorization": proxyAuth,
|
|
389
|
-
Host: parsedUrl.host,
|
|
390
|
-
},
|
|
391
|
-
},
|
|
392
|
-
(res) => {
|
|
393
|
-
const chunks: Buffer[] = [];
|
|
394
|
-
let byteCount = 0;
|
|
395
|
-
res.on("data", (chunk: Buffer) => {
|
|
396
|
-
byteCount += chunk.length;
|
|
397
|
-
if (byteCount <= MAX_BODY_BYTES) chunks.push(chunk);
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
let finalized = false;
|
|
401
|
-
const finalize = () => {
|
|
402
|
-
if (finalized) return;
|
|
403
|
-
finalized = true;
|
|
404
|
-
clearTimeout(timeout);
|
|
405
|
-
const body = Buffer.concat(chunks).toString("utf-8").substring(0, MAX_BODY_BYTES);
|
|
406
|
-
const respHeaders: Record<string, string> = {};
|
|
407
|
-
for (const [k, v] of Object.entries(res.headers)) {
|
|
408
|
-
if (v) respHeaders[k] = Array.isArray(v) ? v.join(", ") : v;
|
|
409
|
-
}
|
|
410
|
-
resolve({ status: res.statusCode ?? 0, headers: respHeaders, body });
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
res.on("end", finalize);
|
|
414
|
-
res.on("close", finalize);
|
|
415
|
-
res.on("error", (err) => {
|
|
416
|
-
clearTimeout(timeout);
|
|
417
|
-
reject(err);
|
|
418
|
-
});
|
|
419
|
-
},
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
req.on("error", (err) => {
|
|
423
|
-
clearTimeout(timeout);
|
|
424
|
-
reject(err);
|
|
425
|
-
});
|
|
426
|
-
req.end();
|
|
427
|
-
}
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
returnData.push({
|
|
431
|
-
json: {
|
|
432
|
-
status: result.status,
|
|
433
|
-
headers: result.headers,
|
|
434
|
-
body: result.body.substring(0, MAX_BODY_TRUNCATE),
|
|
435
|
-
url,
|
|
436
|
-
method: upperMethod,
|
|
437
|
-
proxyType,
|
|
438
|
-
country: country || undefined,
|
|
439
|
-
},
|
|
440
|
-
});
|
|
441
|
-
} else if (operation === "getProxyConfig") {
|
|
442
|
-
const result = await auth.apiRequest("GET", "/api/proxy/config");
|
|
443
|
-
returnData.push({ json: result as IDataObject });
|
|
444
|
-
} else if (operation === "listActiveSessions") {
|
|
445
|
-
const result = await auth.apiRequest("GET", "/api/sessions/active");
|
|
446
|
-
returnData.push({ json: result as IDataObject });
|
|
447
|
-
} else if (operation === "getProxyStatus") {
|
|
448
|
-
const result = await auth.apiRequest("GET", "/api/proxy/status");
|
|
449
|
-
returnData.push({ json: result as IDataObject });
|
|
450
|
-
}
|
|
451
|
-
} catch (err) {
|
|
452
|
-
if (this.continueOnFail()) {
|
|
453
|
-
returnData.push({
|
|
454
|
-
json: {
|
|
455
|
-
error: sanitizeError(err instanceof Error ? err.message : String(err)),
|
|
456
|
-
},
|
|
457
|
-
});
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
throw new NodeOperationError(
|
|
461
|
-
this.getNode(),
|
|
462
|
-
sanitizeError(err instanceof Error ? err.message : String(err)),
|
|
463
|
-
{ itemIndex: i },
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
return [returnData];
|
|
469
|
-
}
|
|
470
|
-
}
|