@opensecurity/zonzon-core 0.1.2 → 0.1.4

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.
Files changed (71) hide show
  1. package/dist/audit.d.ts +10 -0
  2. package/dist/audit.js +39 -0
  3. package/dist/cache-layer.test.d.ts +1 -0
  4. package/dist/cache-layer.test.js +205 -0
  5. package/dist/cache-multi-question.test.d.ts +1 -0
  6. package/dist/cache-multi-question.test.js +187 -0
  7. package/dist/dns-handler.d.ts +27 -0
  8. package/dist/dns-handler.js +323 -0
  9. package/dist/dns-service.d.ts +45 -0
  10. package/dist/dns-service.js +546 -0
  11. package/dist/dns-service.test.d.ts +1 -0
  12. package/dist/dns-service.test.js +306 -0
  13. package/dist/dns-wireformat.test.d.ts +1 -0
  14. package/dist/dns-wireformat.test.js +669 -0
  15. package/dist/firewall.d.ts +9 -0
  16. package/dist/firewall.js +62 -0
  17. package/dist/http-body-forwarding-integration.test.d.ts +1 -0
  18. package/dist/http-body-forwarding-integration.test.js +318 -0
  19. package/dist/http-body-forwarding.test.d.ts +1 -0
  20. package/dist/http-body-forwarding.test.js +84 -0
  21. package/dist/http-handler.d.ts +21 -0
  22. package/dist/http-handler.js +429 -0
  23. package/dist/http-proxy.d.ts +14 -0
  24. package/dist/http-proxy.js +135 -0
  25. package/dist/http-proxy.test.d.ts +1 -0
  26. package/dist/http-proxy.test.js +375 -0
  27. package/{src/index.ts → dist/index.d.ts} +1 -1
  28. package/dist/index.js +10 -0
  29. package/dist/rate-limiter.d.ts +11 -0
  30. package/dist/rate-limiter.js +33 -0
  31. package/dist/rate-limiter.test.d.ts +1 -0
  32. package/dist/rate-limiter.test.js +149 -0
  33. package/dist/schema.d.ts +12 -0
  34. package/dist/schema.js +126 -0
  35. package/dist/schema.test.d.ts +1 -0
  36. package/dist/schema.test.js +586 -0
  37. package/dist/sni-proxy.d.ts +12 -0
  38. package/dist/sni-proxy.js +141 -0
  39. package/dist/srv-record.test.d.ts +1 -0
  40. package/dist/srv-record.test.js +186 -0
  41. package/dist/tcp-connection-limit.test.d.ts +1 -0
  42. package/dist/tcp-connection-limit.test.js +89 -0
  43. package/dist/types.d.ts +147 -0
  44. package/dist/types.js +34 -0
  45. package/dist/wildcard-matching.test.d.ts +1 -0
  46. package/dist/wildcard-matching.test.js +162 -0
  47. package/package.json +4 -1
  48. package/src/audit.ts +0 -43
  49. package/src/cache-layer.test.ts +0 -236
  50. package/src/cache-multi-question.test.ts +0 -263
  51. package/src/dns-handler.ts +0 -355
  52. package/src/dns-service.test.ts +0 -371
  53. package/src/dns-service.ts +0 -655
  54. package/src/dns-wireformat.test.ts +0 -771
  55. package/src/env.d.ts +0 -1
  56. package/src/firewall.ts +0 -66
  57. package/src/http-body-forwarding-integration.test.ts +0 -357
  58. package/src/http-body-forwarding.test.ts +0 -101
  59. package/src/http-handler.ts +0 -489
  60. package/src/http-proxy.test.ts +0 -440
  61. package/src/http-proxy.ts +0 -148
  62. package/src/rate-limiter.test.ts +0 -144
  63. package/src/rate-limiter.ts +0 -50
  64. package/src/schema.test.ts +0 -685
  65. package/src/schema.ts +0 -137
  66. package/src/sni-proxy.ts +0 -164
  67. package/src/srv-record.test.ts +0 -211
  68. package/src/tcp-connection-limit.test.ts +0 -110
  69. package/src/types.ts +0 -168
  70. package/src/wildcard-matching.test.ts +0 -196
  71. package/tsconfig.json +0 -9
@@ -0,0 +1,429 @@
1
+ import * as http from "http";
2
+ import * as net from "net";
3
+ import * as dns from "dns/promises";
4
+ import { HttpProxyService } from "./http-proxy.js";
5
+ import { audit } from "./audit.js";
6
+ import { firewallEngine } from "./firewall.js";
7
+ var CircuitState;
8
+ (function (CircuitState) {
9
+ CircuitState[CircuitState["CLOSED"] = 0] = "CLOSED";
10
+ CircuitState[CircuitState["OPEN"] = 1] = "OPEN";
11
+ CircuitState[CircuitState["HALF_OPEN"] = 2] = "HALF_OPEN";
12
+ })(CircuitState || (CircuitState = {}));
13
+ class ProxyCircuitBreaker {
14
+ state = CircuitState.CLOSED;
15
+ failures = 0;
16
+ lastFailureTime = 0;
17
+ threshold = 5;
18
+ resetTimeoutMs = 10000;
19
+ async execute(action) {
20
+ const now = Date.now();
21
+ if (this.state === CircuitState.OPEN) {
22
+ if (now - this.lastFailureTime > this.resetTimeoutMs) {
23
+ this.state = CircuitState.HALF_OPEN;
24
+ }
25
+ else {
26
+ throw new Error("Target Offline");
27
+ }
28
+ }
29
+ try {
30
+ const result = await action();
31
+ if (this.state === CircuitState.HALF_OPEN) {
32
+ this.reset();
33
+ }
34
+ return result;
35
+ }
36
+ catch (error) {
37
+ this.recordFailure(now);
38
+ throw error;
39
+ }
40
+ }
41
+ recordFailure(time) {
42
+ this.failures++;
43
+ this.lastFailureTime = time;
44
+ if (this.failures >= this.threshold) {
45
+ this.state = CircuitState.OPEN;
46
+ }
47
+ }
48
+ reset() {
49
+ this.state = CircuitState.CLOSED;
50
+ this.failures = 0;
51
+ }
52
+ }
53
+ export class HttpHandler {
54
+ dnsServer;
55
+ proxyService;
56
+ port;
57
+ config;
58
+ server = null;
59
+ circuitBreakers = new Map();
60
+ activeConnections = new Set();
61
+ constructor(dnsServer, config, port) {
62
+ this.dnsServer = dnsServer;
63
+ this.proxyService = new HttpProxyService();
64
+ this.config = config;
65
+ this.port = config.httpPort ?? port ?? 80;
66
+ }
67
+ getCircuitBreaker(upstream) {
68
+ if (!this.circuitBreakers.has(upstream)) {
69
+ this.circuitBreakers.set(upstream, new ProxyCircuitBreaker());
70
+ }
71
+ return this.circuitBreakers.get(upstream);
72
+ }
73
+ findWildcardHost(config, hostname) {
74
+ const normalizedName = hostname.toLowerCase().replace(/\.$/, "");
75
+ const labels = normalizedName.split(".");
76
+ for (let i = 0; i < labels.length - 1; i++) {
77
+ const suffix = labels.slice(i).join(".");
78
+ const wildcardKey = "*." + suffix;
79
+ if (config.hosts[wildcardKey]) {
80
+ return config.hosts[wildcardKey];
81
+ }
82
+ }
83
+ return undefined;
84
+ }
85
+ injectCustomHeaders(responseHeaders, hostConfig) {
86
+ if (!hostConfig.http_proxy?.enabled)
87
+ return;
88
+ for (const [key, value] of Object.entries(hostConfig.http_proxy.headers)) {
89
+ const sanitized = this.proxyService.sanitizeHeader(value);
90
+ if (sanitized) {
91
+ responseHeaders.set(key, sanitized);
92
+ }
93
+ }
94
+ }
95
+ async handleConnect(req, clientSocket, head) {
96
+ const clientIp = req.socket.remoteAddress || "unknown";
97
+ const targetUrl = req.url || "";
98
+ const [hostname, portStr] = targetUrl.split(':');
99
+ const port = portStr ? parseInt(portStr, 10) : 443;
100
+ try {
101
+ await this.proxyService.validateTargetFirewall(`https://${hostname}:${port}`, this.config.firewall);
102
+ audit.http(clientIp, "CONNECT", hostname, `:${port}`, 200, "TCP Tunnel Established");
103
+ const srvSocket = net.connect(port, hostname, () => {
104
+ clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
105
+ if (head && head.length > 0) {
106
+ srvSocket.write(head);
107
+ }
108
+ srvSocket.pipe(clientSocket);
109
+ clientSocket.pipe(srvSocket);
110
+ });
111
+ this.activeConnections.add(srvSocket);
112
+ srvSocket.on('close', () => this.activeConnections.delete(srvSocket));
113
+ srvSocket.on('error', (err) => {
114
+ audit.error(`Upstream tunnel fault on ${hostname}:${port} - ${err.message}`);
115
+ if (!clientSocket.destroyed)
116
+ clientSocket.destroy();
117
+ });
118
+ clientSocket.on('error', (err) => {
119
+ if (!srvSocket.destroyed)
120
+ srvSocket.destroy();
121
+ });
122
+ }
123
+ catch (err) {
124
+ audit.http(clientIp, "CONNECT", hostname, `:${port}`, 403, "Blocked by Firewall");
125
+ if (!clientSocket.destroyed) {
126
+ clientSocket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
127
+ clientSocket.destroy();
128
+ }
129
+ }
130
+ }
131
+ async handleForwardProxy(req, res) {
132
+ const clientIp = req.socket.remoteAddress || "unknown";
133
+ const reqMethod = req.method || "GET";
134
+ const reqUrl = req.url || "/";
135
+ const targetUrl = new URL(reqUrl);
136
+ const hostname = targetUrl.hostname;
137
+ try {
138
+ await this.proxyService.validateTargetFirewall(targetUrl.toString(), this.config.firewall);
139
+ }
140
+ catch (fwErr) {
141
+ audit.http(clientIp, reqMethod, hostname, targetUrl.pathname, 403, "Blocked by L3/L7 Firewall");
142
+ res.writeHead(403, { "Content-Type": "text/html" });
143
+ res.end(`<h1>403 Forbidden</h1><p>${fwErr.message}</p>`);
144
+ return;
145
+ }
146
+ const headers = new Headers();
147
+ for (const [k, v] of Object.entries(req.headers)) {
148
+ if (Array.isArray(v)) {
149
+ headers.set(k, v.join(", "));
150
+ }
151
+ else if (typeof v === "string") {
152
+ headers.set(k, v);
153
+ }
154
+ }
155
+ const hopByHop = this.proxyService.getHopByHopHeaders();
156
+ for (const h of hopByHop) {
157
+ headers.delete(h);
158
+ }
159
+ const maxBodyBytes = 5 * 1024 * 1024;
160
+ let bodyBuffer = undefined;
161
+ if (reqMethod !== "GET" && reqMethod !== "HEAD") {
162
+ const chunks = [];
163
+ let totalSize = 0;
164
+ for await (const chunk of req) {
165
+ totalSize += chunk.length;
166
+ if (totalSize > maxBodyBytes) {
167
+ audit.http(clientIp, reqMethod, hostname, reqUrl, 413, targetUrl.hostname);
168
+ res.writeHead(413, { "Content-Type": "text/html" });
169
+ res.end("<h1>413 Payload Too Large</h1>");
170
+ return;
171
+ }
172
+ chunks.push(chunk);
173
+ }
174
+ if (chunks.length > 0) {
175
+ bodyBuffer = Buffer.concat(chunks);
176
+ }
177
+ }
178
+ const proxiedReqInit = {
179
+ method: reqMethod,
180
+ headers,
181
+ };
182
+ if (bodyBuffer) {
183
+ proxiedReqInit.body = bodyBuffer;
184
+ proxiedReqInit.duplex = "half";
185
+ }
186
+ const proxiedReq = new Request(targetUrl.toString(), proxiedReqInit);
187
+ const breaker = this.getCircuitBreaker(targetUrl.hostname);
188
+ try {
189
+ const proxyResp = await breaker.execute(() => fetch(proxiedReq));
190
+ const outHeaders = {};
191
+ proxyResp.headers.forEach((value, key) => {
192
+ if (!hopByHop.includes(key.toLowerCase())) {
193
+ outHeaders[key] = value;
194
+ }
195
+ });
196
+ audit.http(clientIp, reqMethod, hostname, targetUrl.pathname, proxyResp.status, targetUrl.toString());
197
+ res.writeHead(proxyResp.status, outHeaders);
198
+ const responseBuffer = Buffer.from(await proxyResp.arrayBuffer());
199
+ res.end(responseBuffer);
200
+ }
201
+ catch (err) {
202
+ audit.http(clientIp, reqMethod, hostname, targetUrl.pathname, 502, "Upstream Timeout or Fault");
203
+ res.writeHead(502, { "Content-Type": "text/html" });
204
+ res.end(`<h1>502 Bad Gateway</h1>`);
205
+ }
206
+ }
207
+ async handleRequest(req, res) {
208
+ const clientIp = req.socket.remoteAddress || "unknown";
209
+ const reqMethod = req.method || "GET";
210
+ const reqUrl = req.url || "/";
211
+ if (reqUrl.startsWith("http://")) {
212
+ return this.handleForwardProxy(req, res);
213
+ }
214
+ const rawHost = req.headers.host || "";
215
+ const hostname = rawHost.split(":")[0];
216
+ try {
217
+ if (!hostname) {
218
+ audit.http(clientIp, reqMethod, "UNKNOWN", reqUrl, 400, "Missing Host Header");
219
+ res.writeHead(400);
220
+ res.end("Bad Request");
221
+ return;
222
+ }
223
+ let hostConfig = this.config.hosts[hostname];
224
+ if (!hostConfig) {
225
+ const wildConfig = this.findWildcardHost(this.config, hostname);
226
+ if (wildConfig)
227
+ hostConfig = wildConfig;
228
+ }
229
+ if (hostConfig) {
230
+ const redirect = this.proxyService.checkRedirect(hostConfig);
231
+ if (redirect) {
232
+ try {
233
+ await this.proxyService.validateTargetFirewall(redirect.target, this.config.firewall);
234
+ audit.http(clientIp, reqMethod, hostname, reqUrl, redirect.code, redirect.target);
235
+ res.writeHead(redirect.code, { Location: redirect.target });
236
+ res.end();
237
+ return;
238
+ }
239
+ catch (err) {
240
+ audit.http(clientIp, reqMethod, hostname, reqUrl, 403, "Blocked by L3 Firewall");
241
+ res.writeHead(403, { "Content-Type": "text/html" });
242
+ res.end("<h1>403 Forbidden</h1>");
243
+ return;
244
+ }
245
+ }
246
+ if (hostConfig.http_proxy?.enabled && hostConfig.http_proxy.upstream) {
247
+ const upstreamBase = new URL(hostConfig.http_proxy.upstream);
248
+ try {
249
+ await this.proxyService.validateTargetFirewall(upstreamBase.toString(), this.config.firewall);
250
+ }
251
+ catch (fwErr) {
252
+ audit.http(clientIp, reqMethod, hostname, reqUrl, 403, "Blocked by L3 Firewall");
253
+ res.writeHead(403, { "Content-Type": "text/html" });
254
+ res.end(`<h1>403 Forbidden</h1><p>${fwErr.message}</p>`);
255
+ return;
256
+ }
257
+ let safePath = "/";
258
+ try {
259
+ const parsedPath = new URL(reqUrl, "http://safe.local");
260
+ safePath = parsedPath.pathname + parsedPath.search;
261
+ }
262
+ catch {
263
+ safePath = "/";
264
+ }
265
+ const targetUrl = new URL(safePath, upstreamBase);
266
+ const headers = new Headers();
267
+ for (const [k, v] of Object.entries(req.headers)) {
268
+ if (Array.isArray(v)) {
269
+ headers.set(k, v.join(", "));
270
+ }
271
+ else if (typeof v === "string") {
272
+ headers.set(k, v);
273
+ }
274
+ }
275
+ const shouldForwardBody = hostConfig.http_proxy.forwardRequestBody;
276
+ const maxBodyBytes = hostConfig.http_proxy.maxRequestBodyBytes ?? 5 * 1024 * 1024;
277
+ let bodyBuffer = undefined;
278
+ if (reqMethod !== "GET" && reqMethod !== "HEAD") {
279
+ const chunks = [];
280
+ let totalSize = 0;
281
+ for await (const chunk of req) {
282
+ totalSize += chunk.length;
283
+ if (totalSize > maxBodyBytes) {
284
+ audit.http(clientIp, reqMethod, hostname, reqUrl, 413, upstreamBase.hostname);
285
+ res.writeHead(413, { "Content-Type": "text/html" });
286
+ res.end("<h1>413 Payload Too Large</h1>");
287
+ return;
288
+ }
289
+ chunks.push(chunk);
290
+ }
291
+ if (chunks.length > 0) {
292
+ bodyBuffer = Buffer.concat(chunks);
293
+ }
294
+ }
295
+ const proxiedReqInit = {
296
+ method: reqMethod,
297
+ headers,
298
+ };
299
+ if (shouldForwardBody && bodyBuffer) {
300
+ proxiedReqInit.body = bodyBuffer;
301
+ proxiedReqInit.duplex = "half";
302
+ }
303
+ const proxiedReq = new Request(targetUrl.toString(), proxiedReqInit);
304
+ const breaker = this.getCircuitBreaker(upstreamBase.hostname);
305
+ const proxyResp = await breaker.execute(() => fetch(proxiedReq));
306
+ const responseHeaders = new Headers(proxyResp.headers);
307
+ this.injectCustomHeaders(responseHeaders, hostConfig);
308
+ const outHeaders = {};
309
+ responseHeaders.forEach((value, key) => {
310
+ outHeaders[key] = value;
311
+ });
312
+ audit.http(clientIp, reqMethod, hostname, safePath, proxyResp.status, targetUrl.toString());
313
+ res.writeHead(proxyResp.status, outHeaders);
314
+ const responseBuffer = Buffer.from(await proxyResp.arrayBuffer());
315
+ res.end(responseBuffer);
316
+ return;
317
+ }
318
+ }
319
+ if (firewallEngine.evaluateDomain(hostname, this.config.firewall) === "DENY") {
320
+ audit.http(clientIp, reqMethod, hostname, reqUrl, 403, "Blocked by Domain Firewall");
321
+ res.writeHead(403);
322
+ res.end("Forbidden");
323
+ return;
324
+ }
325
+ const records = await dns.resolve(hostname);
326
+ const targetIps = records.filter(ip => typeof ip === "string");
327
+ if (targetIps.length === 0)
328
+ throw new Error("NXDOMAIN");
329
+ const targetIp = targetIps[0];
330
+ if (firewallEngine.evaluateIp(targetIp, this.config.firewall) === "DENY") {
331
+ audit.http(clientIp, reqMethod, hostname, reqUrl, 403, "Blocked by IP Firewall");
332
+ res.writeHead(403);
333
+ res.end("Forbidden");
334
+ return;
335
+ }
336
+ const targetUrl = new URL(reqUrl, `http://${hostname}`);
337
+ const headers = new Headers();
338
+ for (const [k, v] of Object.entries(req.headers)) {
339
+ if (Array.isArray(v))
340
+ headers.set(k, v.join(", "));
341
+ else if (typeof v === "string")
342
+ headers.set(k, v);
343
+ }
344
+ const proxiedReqInit = { method: reqMethod, headers };
345
+ if (reqMethod !== "GET" && reqMethod !== "HEAD") {
346
+ const chunks = [];
347
+ for await (const chunk of req)
348
+ chunks.push(chunk);
349
+ if (chunks.length > 0) {
350
+ proxiedReqInit.body = Buffer.concat(chunks);
351
+ proxiedReqInit.duplex = "half";
352
+ }
353
+ }
354
+ const proxiedReq = new Request(targetUrl.toString(), proxiedReqInit);
355
+ const breaker = this.getCircuitBreaker(targetUrl.hostname);
356
+ const proxyResp = await breaker.execute(() => fetch(proxiedReq));
357
+ const outHeaders = {};
358
+ proxyResp.headers.forEach((value, key) => { outHeaders[key] = value; });
359
+ audit.http(clientIp, reqMethod, hostname, reqUrl, proxyResp.status, targetIp);
360
+ res.writeHead(proxyResp.status, outHeaders);
361
+ res.end(Buffer.from(await proxyResp.arrayBuffer()));
362
+ }
363
+ catch (err) {
364
+ const message = err instanceof Error ? err.message : String(err);
365
+ audit.error(`HTTP request failed: ${message}`);
366
+ if (message.includes("Security Block") || message.includes("Firewall") || message.includes("Blocked")) {
367
+ audit.http(clientIp, reqMethod, hostname, reqUrl, 403, "Blocked for Security (SSRF/Firewall)");
368
+ res.writeHead(403, { "Content-Type": "text/html" });
369
+ res.end(`<h1>403 Forbidden</h1>`);
370
+ return;
371
+ }
372
+ audit.http(clientIp, reqMethod, hostname, reqUrl, 502, "Upstream Offline/Timeout");
373
+ res.writeHead(502, { "Content-Type": "text/html" });
374
+ res.end(`<h1>502 Bad Gateway</h1>`);
375
+ return;
376
+ }
377
+ }
378
+ start() {
379
+ return new Promise((resolve, reject) => {
380
+ this.server = http.createServer((req, res) => {
381
+ this.handleRequest(req, res).catch((err) => {
382
+ audit.error(`HTTP interface critical error: ${err}`);
383
+ if (!res.headersSent) {
384
+ res.writeHead(500, { "Content-Type": "text/html" });
385
+ res.end("<h1>500 Internal Server Error</h1>");
386
+ }
387
+ });
388
+ });
389
+ this.server.on("connect", (req, clientSocket, head) => {
390
+ this.handleConnect(req, clientSocket, head).catch((err) => {
391
+ audit.error(`TCP CONNECT critical error: ${err}`);
392
+ if (!clientSocket.destroyed) {
393
+ clientSocket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
394
+ clientSocket.destroy();
395
+ }
396
+ });
397
+ });
398
+ this.server.on("connection", (socket) => {
399
+ this.activeConnections.add(socket);
400
+ socket.on("close", () => {
401
+ this.activeConnections.delete(socket);
402
+ });
403
+ });
404
+ this.server.on('error', (err) => reject(err));
405
+ this.server.listen(this.port, "0.0.0.0", () => {
406
+ audit.system(`L7 Sandbox Firewall routing internally on boundary :${this.port}`);
407
+ resolve();
408
+ });
409
+ });
410
+ }
411
+ async stop() {
412
+ if (this.server) {
413
+ for (const socket of this.activeConnections) {
414
+ socket.destroy();
415
+ }
416
+ this.activeConnections.clear();
417
+ if ('closeAllConnections' in this.server) {
418
+ this.server.closeAllConnections();
419
+ }
420
+ await new Promise((resolve) => {
421
+ this.server.close(() => resolve());
422
+ });
423
+ this.server = null;
424
+ }
425
+ }
426
+ getPort() {
427
+ return this.port;
428
+ }
429
+ }
@@ -0,0 +1,14 @@
1
+ import { HostConfig, ProxiedRequest, ModifiedHeaders, FirewallConfig } from "./types.js";
2
+ export declare class HttpProxyService {
3
+ private isRestrictedCloudMetadata;
4
+ validateTargetFirewall(targetUrl: string, fw?: FirewallConfig): Promise<void>;
5
+ getUpstreamHeaders(config: HostConfig, originalRequest: ProxiedRequest): ModifiedHeaders;
6
+ checkRedirect(config: HostConfig): {
7
+ code: number;
8
+ target: string;
9
+ } | null;
10
+ sanitizeHeader(value: string): string | null;
11
+ isValidHeaderName(name: string): boolean;
12
+ getHopByHopHeaders(): string[];
13
+ calculateTimeout(config: HostConfig): number;
14
+ }
@@ -0,0 +1,135 @@
1
+ import * as net from "net";
2
+ import * as dns from "dns/promises";
3
+ import { firewallEngine } from "./firewall.js";
4
+ export class HttpProxyService {
5
+ isRestrictedCloudMetadata(ip) {
6
+ if (!net.isIPv4(ip))
7
+ return false;
8
+ const parts = ip.split('.').map(Number);
9
+ return (parts[0] === 169 && parts[1] === 254) || parts[0] === 0;
10
+ }
11
+ async validateTargetFirewall(targetUrl, fw) {
12
+ const parsed = new URL(targetUrl);
13
+ const host = parsed.hostname;
14
+ const isLiteralIp = net.isIP(host) !== 0;
15
+ if (fw && !isLiteralIp) {
16
+ if (firewallEngine.evaluateDomain(host, fw) === "DENY") {
17
+ throw new Error(`Domain Blocked: '${host}'`);
18
+ }
19
+ }
20
+ let targetIps = [];
21
+ if (isLiteralIp) {
22
+ targetIps = [host];
23
+ }
24
+ else {
25
+ try {
26
+ const records = await dns.resolve(host);
27
+ targetIps = records.filter(ip => typeof ip === 'string');
28
+ }
29
+ catch {
30
+ throw new Error(`Resolution Fault: '${host}'`);
31
+ }
32
+ }
33
+ for (const ip of targetIps) {
34
+ if (this.isRestrictedCloudMetadata(ip)) {
35
+ throw new Error(`Restricted IP: (${ip})`);
36
+ }
37
+ if (fw) {
38
+ if (firewallEngine.evaluateIp(ip, fw) === "DENY") {
39
+ throw new Error(`IP Blocked: ${ip}`);
40
+ }
41
+ }
42
+ }
43
+ }
44
+ getUpstreamHeaders(config, originalRequest) {
45
+ const result = {
46
+ upstreamHeaders: {},
47
+ clientResponseHeaders: {},
48
+ };
49
+ if (!config.http_proxy || !config.http_proxy.enabled) {
50
+ return result;
51
+ }
52
+ for (const [key, value] of Object.entries(originalRequest.headers)) {
53
+ const lowerKey = key.toLowerCase();
54
+ if (!["connection", "keep-alive", "te", "transfer-encoding", "upgrade", "proxy-authorization"].includes(lowerKey)) {
55
+ result.clientResponseHeaders[key] = value;
56
+ }
57
+ }
58
+ for (const [key, value] of Object.entries(config.http_proxy.headers)) {
59
+ const sanitized = this.sanitizeHeader(value);
60
+ if (sanitized) {
61
+ result.upstreamHeaders[key] = sanitized;
62
+ result.clientResponseHeaders[key] = sanitized;
63
+ }
64
+ }
65
+ result.clientResponseHeaders["X-Proxy"] = "zonzon";
66
+ if (config.http_proxy.forwardRequestBody && originalRequest.body) {
67
+ const maxBodyBytes = config.http_proxy.maxRequestBodyBytes ?? 5 * 1024 * 1024;
68
+ if (originalRequest.body.length <= maxBodyBytes) {
69
+ result.upstreamHeaders["X-Body-Forwarded"] = "true";
70
+ result.upstreamHeaders["X-Body-Size"] = String(originalRequest.body.length);
71
+ }
72
+ else {
73
+ throw new Error(`Payload Limit Exceeded`);
74
+ }
75
+ }
76
+ return result;
77
+ }
78
+ checkRedirect(config) {
79
+ if (!config.redirect || !config.redirect.enabled) {
80
+ return null;
81
+ }
82
+ const { code, target } = config.redirect;
83
+ if (![301, 302, 303, 307, 308].includes(code)) {
84
+ return null;
85
+ }
86
+ try {
87
+ const parsed = new URL(target);
88
+ if (!parsed.protocol || !parsed.hostname) {
89
+ return null;
90
+ }
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ return { code, target };
96
+ }
97
+ sanitizeHeader(value) {
98
+ if (typeof value !== "string")
99
+ return null;
100
+ if (/[\r\n\t]/.test(value))
101
+ return null;
102
+ if (/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(value))
103
+ return null;
104
+ if (/%0[dD]/i.test(value) || /%0[aA]/.test(value))
105
+ return null;
106
+ if (value.length > 8192)
107
+ return null;
108
+ return value;
109
+ }
110
+ isValidHeaderName(name) {
111
+ if (typeof name !== "string" || name.length === 0)
112
+ return false;
113
+ if (name.length > 256)
114
+ return false;
115
+ return /^[a-zA-Z0-9!#$%&'*+\-.^_`|~]+$/.test(name);
116
+ }
117
+ getHopByHopHeaders() {
118
+ return [
119
+ "connection",
120
+ "keep-alive",
121
+ "proxy-authenticate",
122
+ "proxy-authorization",
123
+ "te",
124
+ "trailer",
125
+ "transfer-encoding",
126
+ "upgrade",
127
+ ];
128
+ }
129
+ calculateTimeout(config) {
130
+ if (config.http_proxy?.enabled) {
131
+ return Math.max(1000, Math.min(30000, 5000));
132
+ }
133
+ return 0;
134
+ }
135
+ }
@@ -0,0 +1 @@
1
+ export {};