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