@opensecurity/zonzon-core 0.1.2 → 0.1.3

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 +124 -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 +145 -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
@@ -1,440 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert";
3
- import { HttpProxyService } from "./http-proxy.js";
4
- import { HostConfig } from "./types.js";
5
-
6
- function makeConfig(overrides: Partial<HostConfig> = {}): HostConfig {
7
- return {
8
- records: [{ type: "A", address: "127.0.0.1" }],
9
- http_proxy: undefined,
10
- redirect: undefined,
11
- ...overrides,
12
- };
13
- }
14
-
15
- function makeProxyEnabledConfig(): HostConfig {
16
- return makeConfig({
17
- http_proxy: {
18
- enabled: true,
19
- upstream: "http://upstream.example.com",
20
- headers: {
21
- "X-Custom": "custom-value",
22
- "X-Env": "production",
23
- },
24
- },
25
- });
26
- }
27
-
28
- function makeRedirectConfig(): HostConfig {
29
- return makeConfig({
30
- redirect: {
31
- enabled: true,
32
- code: 301,
33
- target: "https://target.example.com/path",
34
- },
35
- });
36
- }
37
-
38
- describe("HttpProxyService - Header Sanitization", () => {
39
- const proxy = new HttpProxyService();
40
-
41
- it("accepts normal alphanumeric header values", () => {
42
- assert.strictEqual(proxy.sanitizeHeader("normal-value"), "normal-value");
43
- });
44
-
45
- it("rejects header values with CR characters", () => {
46
- assert.strictEqual(proxy.sanitizeHeader("value\x0dwithCR"), null);
47
- });
48
-
49
- it("rejects header values with LF characters", () => {
50
- assert.strictEqual(proxy.sanitizeHeader("value\nwithLF"), null);
51
- });
52
-
53
- it("rejects header values with CRLF combinations", () => {
54
- assert.strictEqual(proxy.sanitizeHeader("value\r\nInjected: true"), null);
55
- });
56
-
57
- it("rejects header values exceeding 8192 characters", () => {
58
- const longValue = "x".repeat(8193);
59
- assert.strictEqual(proxy.sanitizeHeader(longValue), null);
60
- });
61
-
62
- it("accepts header values at exactly 8192 characters", () => {
63
- const maxLen = "x".repeat(8192);
64
- assert.strictEqual(proxy.sanitizeHeader(maxLen), maxLen);
65
- });
66
-
67
- it("rejects non-string values", () => {
68
- assert.strictEqual(proxy.sanitizeHeader(123 as unknown as string), null);
69
- assert.strictEqual(proxy.sanitizeHeader(null as unknown as string), null);
70
- assert.strictEqual(proxy.sanitizeHeader(undefined as unknown as string), null);
71
- assert.strictEqual(proxy.sanitizeHeader({} as unknown as string), null);
72
- });
73
-
74
- it("accepts header values with safe special characters", () => {
75
- const value = "Bearer abc123.def456 ghi789!@#$%^&*()";
76
- assert.strictEqual(proxy.sanitizeHeader(value), value);
77
- });
78
- });
79
-
80
- describe("HttpProxyService - Header Name Validation", () => {
81
- const proxy = new HttpProxyService();
82
-
83
- it("accepts valid RFC 7230 token header names", () => {
84
- assert.strictEqual(proxy.isValidHeaderName("Content-Type"), true);
85
- assert.strictEqual(proxy.isValidHeaderName("X-Custom-Header"), true);
86
- assert.strictEqual(proxy.isValidHeaderName("Authorization"), true);
87
- assert.strictEqual(proxy.isValidHeaderName("X-B3-TraceId"), true);
88
- });
89
-
90
- it("accepts header names with RFC-valid special characters", () => {
91
- assert.strictEqual(proxy.isValidHeaderName("X-Test_Header"), true);
92
- assert.strictEqual(proxy.isValidHeaderName("X-Test+Header"), true);
93
- assert.strictEqual(proxy.isValidHeaderName("X-Test~Header"), true);
94
- });
95
-
96
- it("rejects empty header names", () => {
97
- assert.strictEqual(proxy.isValidHeaderName(""), false);
98
- });
99
-
100
- it("rejects header names exceeding 256 characters", () => {
101
- const longName = "x".repeat(257);
102
- assert.strictEqual(proxy.isValidHeaderName(longName), false);
103
- });
104
-
105
- it("rejects header names with spaces", () => {
106
- assert.strictEqual(proxy.isValidHeaderName("X- Evil"), false);
107
- });
108
-
109
- it("rejects header names with newlines (CRLF injection)", () => {
110
- assert.strictEqual(proxy.isValidHeaderName("X-Bad\r\nInjected"), false);
111
- assert.strictEqual(proxy.isValidHeaderName("X-Bad\nInjected"), false);
112
- });
113
-
114
- it("rejects header names with forward slashes", () => {
115
- assert.strictEqual(proxy.isValidHeaderName("X/Path/Header"), false);
116
- });
117
-
118
- it("rejects header names with backslashes", () => {
119
- assert.strictEqual(proxy.isValidHeaderName("X\\Backslash"), false);
120
- });
121
- });
122
-
123
- describe("HttpProxyService - Hop-by-Hop Headers", () => {
124
- const proxy = new HttpProxyService();
125
-
126
- it("returns all hop-by-hop headers to exclude", () => {
127
- const excluded = proxy.getHopByHopHeaders();
128
- assert.ok(excluded.includes("connection"));
129
- assert.ok(excluded.includes("keep-alive"));
130
- assert.ok(excluded.includes("te"));
131
- assert.ok(excluded.includes("transfer-encoding"));
132
- assert.ok(excluded.includes("upgrade"));
133
- assert.ok(excluded.includes("proxy-authenticate"));
134
- assert.ok(excluded.includes("proxy-authorization"));
135
- assert.ok(excluded.includes("trailer"));
136
- });
137
-
138
- it("excludes hop-by-hop headers from upstream forwarding", () => {
139
- const config = makeProxyEnabledConfig();
140
- const request = {
141
- hostname: "app.loop",
142
- originalUrl: "/",
143
- method: "GET",
144
- headers: {
145
- connection: "keep-alive",
146
- "keep-alive": "timeout=5",
147
- "X-Forwarded-For": "192.168.1.1",
148
- "X-Custom": "custom-value",
149
- "Transfer-Encoding": "chunked",
150
- },
151
- };
152
-
153
- const result = proxy.getUpstreamHeaders(config, request);
154
- assert.ok("X-Forwarded-For" in result.clientResponseHeaders);
155
- assert.strictEqual(result.upstreamHeaders["X-Custom"], "custom-value");
156
- assert.strictEqual(result.clientResponseHeaders["X-Custom"], "custom-value");
157
- });
158
- });
159
-
160
- describe("HttpProxyService - Header Injection", () => {
161
- const proxy = new HttpProxyService();
162
-
163
- it("injects custom headers when proxy is enabled", () => {
164
- const config = makeProxyEnabledConfig();
165
- const request = {
166
- hostname: "app.loop",
167
- originalUrl: "/",
168
- method: "GET",
169
- headers: {},
170
- };
171
-
172
- const result = proxy.getUpstreamHeaders(config, request);
173
- assert.strictEqual(result.upstreamHeaders["X-Custom"], "custom-value");
174
- assert.strictEqual(result.upstreamHeaders["X-Env"], "production");
175
- assert.strictEqual(result.clientResponseHeaders["X-Proxy"], "zonzon");
176
- });
177
-
178
- it("does not inject any headers when proxy is disabled", () => {
179
- const config = makeConfig({ http_proxy: { enabled: false, upstream: "", headers: {} } });
180
- const request = {
181
- hostname: "app.loop",
182
- originalUrl: "/",
183
- method: "GET",
184
- headers: {},
185
- };
186
-
187
- const result = proxy.getUpstreamHeaders(config, request);
188
- assert.strictEqual(Object.keys(result.upstreamHeaders).length, 0);
189
- assert.ok(!result.clientResponseHeaders["X-Proxy"]);
190
- });
191
-
192
- it("does not inject any headers when proxy config is absent", () => {
193
- const config = makeConfig();
194
- const request = {
195
- hostname: "app.loop",
196
- originalUrl: "/",
197
- method: "GET",
198
- headers: {},
199
- };
200
-
201
- const result = proxy.getUpstreamHeaders(config, request);
202
- assert.strictEqual(Object.keys(result.upstreamHeaders).length, 0);
203
- assert.ok(!result.clientResponseHeaders["X-Proxy"]);
204
- });
205
-
206
- it("adds X-Proxy identification header to client responses", () => {
207
- const config = makeProxyEnabledConfig();
208
- const request = {
209
- hostname: "app.loop",
210
- originalUrl: "/index.html",
211
- method: "GET",
212
- headers: {},
213
- };
214
-
215
- const result = proxy.getUpstreamHeaders(config, request);
216
- assert.strictEqual(result.clientResponseHeaders["X-Proxy"], "zonzon");
217
- });
218
-
219
- it("passes through non-hop-by-hop original headers to client response", () => {
220
- const config = makeProxyEnabledConfig();
221
- const request = {
222
- hostname: "app.loop",
223
- originalUrl: "/",
224
- method: "GET",
225
- headers: {
226
- Accept: "text/html",
227
- "User-Agent": "test-agent/1.0",
228
- "X-Request-Id": "abc-123",
229
- },
230
- };
231
-
232
- const result = proxy.getUpstreamHeaders(config, request);
233
- assert.strictEqual(result.clientResponseHeaders["Accept"], "text/html");
234
- assert.strictEqual(result.clientResponseHeaders["User-Agent"], "test-agent/1.0");
235
- assert.strictEqual(result.clientResponseHeaders["X-Request-Id"], "abc-123");
236
- });
237
-
238
- it("strips hop-by-hop headers from client response", () => {
239
- const config = makeProxyEnabledConfig();
240
- const request = {
241
- hostname: "app.loop",
242
- originalUrl: "/",
243
- method: "GET",
244
- headers: {
245
- Connection: "close",
246
- "Keep-Alive": "timeout=10",
247
- TE: "trailers",
248
- "Transfer-Encoding": "chunked",
249
- },
250
- };
251
-
252
- const result = proxy.getUpstreamHeaders(config, request);
253
- assert.ok(!result.clientResponseHeaders["Connection"]);
254
- assert.ok(!result.clientResponseHeaders["Keep-Alive"]);
255
- assert.ok(!result.clientResponseHeaders["TE"]);
256
- assert.ok(!result.clientResponseHeaders["Transfer-Encoding"]);
257
- });
258
-
259
- it("handles case-insensitive hop-by-hop header matching", () => {
260
- const config = makeProxyEnabledConfig();
261
- const request = {
262
- hostname: "app.loop",
263
- originalUrl: "/",
264
- method: "GET",
265
- headers: {
266
- "CONNECTION": "keep-alive",
267
- "KEEP-ALIVE": "timeout=5",
268
- "TRANSFER-ENCODING": "chunked",
269
- },
270
- };
271
-
272
- const result = proxy.getUpstreamHeaders(config, request);
273
- assert.ok(!result.clientResponseHeaders["CONNECTION"]);
274
- assert.ok(!result.clientResponseHeaders["KEEP-ALIVE"]);
275
- assert.ok(!result.clientResponseHeaders["TRANSFER-ENCODING"]);
276
- });
277
- });
278
-
279
- describe("HttpProxyService - Redirect Checks", () => {
280
- const proxy = new HttpProxyService();
281
-
282
- it("returns redirect info when enabled with valid URL", () => {
283
- const config = makeRedirectConfig();
284
- const result = proxy.checkRedirect(config);
285
- assert.strictEqual(result?.code, 301);
286
- assert.strictEqual(result?.target, "https://target.example.com/path");
287
- });
288
-
289
- it("returns null when redirect is not enabled", () => {
290
- const config = makeConfig();
291
- assert.strictEqual(proxy.checkRedirect(config), null);
292
- });
293
-
294
- it("returns null when proxy-only config (no redirect)", () => {
295
- const config = makeProxyEnabledConfig();
296
- assert.strictEqual(proxy.checkRedirect(config), null);
297
- });
298
-
299
- it("rejects redirects with relative URLs", () => {
300
- const config = makeConfig({
301
- redirect: {
302
- enabled: true,
303
- code: 301,
304
- target: "/path/relative",
305
- },
306
- });
307
- assert.strictEqual(proxy.checkRedirect(config), null);
308
- });
309
-
310
- it("rejects redirects with malformed URLs", () => {
311
- const config = makeConfig({
312
- redirect: {
313
- enabled: true,
314
- code: 301,
315
- target: "not a valid url at all",
316
- },
317
- });
318
- assert.strictEqual(proxy.checkRedirect(config), null);
319
- });
320
-
321
- it("rejects redirects with protocol-relative URLs", () => {
322
- const config = makeConfig({
323
- redirect: {
324
- enabled: true,
325
- code: 301,
326
- target: "//evil.example.com",
327
- },
328
- });
329
- assert.strictEqual(proxy.checkRedirect(config), null);
330
- });
331
-
332
- it("accepts redirect with query parameters in target", () => {
333
- const config = makeConfig({
334
- redirect: {
335
- enabled: true,
336
- code: 302,
337
- target: "https://target.example.com/path?key=value&foo=bar",
338
- },
339
- });
340
- const result = proxy.checkRedirect(config);
341
- assert.strictEqual(result?.code, 302);
342
- assert.ok(result?.target.includes("?key=value"));
343
- });
344
-
345
- it("accepts all valid redirect codes (301, 302, 303, 307, 308)", () => {
346
- for (const code of [301, 302, 303, 307, 308]) {
347
- const config = makeConfig({
348
- redirect: { enabled: true, code, target: "https://example.com" },
349
- });
350
- assert.ok(proxy.checkRedirect(config));
351
- }
352
- });
353
-
354
- it("rejects invalid redirect codes (300, 404, 999)", () => {
355
- for (const code of [200, 300, 404, 500, 999]) {
356
- const config = makeConfig({
357
- redirect: { enabled: true, code, target: "https://example.com" },
358
- });
359
- assert.strictEqual(proxy.checkRedirect(config), null);
360
- }
361
- });
362
- });
363
-
364
- describe("HttpProxyService - Timeout", () => {
365
- const proxy = new HttpProxyService();
366
-
367
- it("returns 0 timeout when proxy is disabled", () => {
368
- const config = makeConfig();
369
- assert.strictEqual(proxy.calculateTimeout(config), 0);
370
- });
371
-
372
- it("returns bounded timeout when proxy is enabled", () => {
373
- const config = makeProxyEnabledConfig();
374
- const timeout = proxy.calculateTimeout(config);
375
- assert.ok(timeout >= 1000 && timeout <= 30000);
376
- });
377
-
378
- it("defaults to 5 seconds for enabled proxy", () => {
379
- const config = makeProxyEnabledConfig();
380
- assert.strictEqual(proxy.calculateTimeout(config), 5000);
381
- });
382
- });
383
-
384
- describe("HttpProxyService - Security Edge Cases", () => {
385
- const proxy = new HttpProxyService();
386
-
387
- it("sanitizes headers containing URL-encoded CR/LF percent sequences in values", () => {
388
- assert.strictEqual(proxy.sanitizeHeader("encoded%0d%0aInjected"), null);
389
- });
390
-
391
- it("rejects header values with tab characters (HTTP smuggling vector)", () => {
392
- assert.strictEqual(proxy.sanitizeHeader("value\twith\ttabs"), null);
393
- });
394
-
395
- it("rejects header values with unicode control characters", () => {
396
- assert.strictEqual(proxy.sanitizeHeader("value\u000b\u000ccontrol"), null);
397
- });
398
-
399
- it("handles proxy config without custom headers gracefully", () => {
400
- const config = makeConfig({
401
- http_proxy: {
402
- enabled: true,
403
- upstream: "http://upstream.example.com",
404
- headers: {},
405
- },
406
- });
407
- const request = {
408
- hostname: "app.loop",
409
- originalUrl: "/",
410
- method: "GET",
411
- headers: {},
412
- };
413
-
414
- const result = proxy.getUpstreamHeaders(config, request);
415
- assert.strictEqual(Object.keys(result.upstreamHeaders).length, 0);
416
- assert.strictEqual(result.clientResponseHeaders["X-Proxy"], "zonzon");
417
- });
418
-
419
- it("handles empty host config records array gracefully", () => {
420
- const config = makeConfig({ http_proxy: undefined });
421
- assert.ok(proxy.checkRedirect(config) === null);
422
- assert.strictEqual(proxy.calculateTimeout(config), 0);
423
- });
424
- });
425
-
426
- describe("HttpProxyService - Header Injection with Sanitization", () => {
427
- const proxy = new HttpProxyService();
428
-
429
- it("sanitizes injected custom headers before passing to response", () => {
430
- const value = "injected\r\nMalicious: header";
431
- const sanitized = proxy.sanitizeHeader(value);
432
- assert.strictEqual(sanitized, null);
433
- });
434
-
435
- it("accepts safe config header values for injection", () => {
436
- const value = "safe-value-with-dashes_and_underscores";
437
- const sanitized = proxy.sanitizeHeader(value);
438
- assert.strictEqual(sanitized, value);
439
- });
440
- });
package/src/http-proxy.ts DELETED
@@ -1,148 +0,0 @@
1
- import * as net from "net";
2
- import * as dns from "dns/promises";
3
- import { HostConfig, ProxiedRequest, ModifiedHeaders, FirewallConfig } from "./types.js";
4
- import { firewallEngine } from "./firewall.js";
5
-
6
- export class HttpProxyService {
7
- private isRestrictedCloudMetadata(ip: string): boolean {
8
- if (!net.isIPv4(ip)) return false;
9
- const parts = ip.split('.').map(Number);
10
- return (parts[0] === 169 && parts[1] === 254) || parts[0] === 0;
11
- }
12
-
13
- async validateTargetFirewall(targetUrl: string, fw?: FirewallConfig): Promise<void> {
14
- const parsed = new URL(targetUrl);
15
- const host = parsed.hostname;
16
- const isLiteralIp = net.isIP(host) !== 0;
17
-
18
- if (fw && !isLiteralIp) {
19
- if (firewallEngine.evaluateDomain(host, fw) === "DENY") {
20
- throw new Error(`Domain Blocked: '${host}'`);
21
- }
22
- }
23
-
24
- let targetIps: string[] = [];
25
-
26
- if (isLiteralIp) {
27
- targetIps = [host];
28
- } else {
29
- try {
30
- const records = await dns.resolve(host);
31
- targetIps = records.filter(ip => typeof ip === 'string') as string[];
32
- } catch {
33
- throw new Error(`Resolution Fault: '${host}'`);
34
- }
35
- }
36
-
37
- for (const ip of targetIps) {
38
- if (this.isRestrictedCloudMetadata(ip)) {
39
- throw new Error(`Restricted IP: (${ip})`);
40
- }
41
-
42
- if (fw) {
43
- if (firewallEngine.evaluateIp(ip, fw) === "DENY") {
44
- throw new Error(`IP Blocked: ${ip}`);
45
- }
46
- }
47
-
48
- }
49
- }
50
-
51
- getUpstreamHeaders(config: HostConfig, originalRequest: ProxiedRequest): ModifiedHeaders {
52
- const result: ModifiedHeaders = {
53
- upstreamHeaders: {},
54
- clientResponseHeaders: {},
55
- };
56
-
57
- if (!config.http_proxy || !config.http_proxy.enabled) {
58
- return result;
59
- }
60
-
61
- for (const [key, value] of Object.entries(originalRequest.headers)) {
62
- const lowerKey = key.toLowerCase();
63
- if (!["connection", "keep-alive", "te", "transfer-encoding", "upgrade", "proxy-authorization"].includes(lowerKey)) {
64
- result.clientResponseHeaders[key] = value;
65
- }
66
- }
67
-
68
- for (const [key, value] of Object.entries(config.http_proxy.headers)) {
69
- const sanitized = this.sanitizeHeader(value);
70
- if (sanitized) {
71
- result.upstreamHeaders[key] = sanitized;
72
- result.clientResponseHeaders[key] = sanitized;
73
- }
74
- }
75
-
76
- result.clientResponseHeaders["X-Proxy"] = "zonzon";
77
-
78
- if (config.http_proxy.forwardRequestBody && originalRequest.body) {
79
- const maxBodyBytes = config.http_proxy.maxRequestBodyBytes ?? 5 * 1024 * 1024;
80
- if (originalRequest.body.length <= maxBodyBytes) {
81
- result.upstreamHeaders["X-Body-Forwarded"] = "true";
82
- result.upstreamHeaders["X-Body-Size"] = String(originalRequest.body.length);
83
- } else {
84
- throw new Error(`Payload Limit Exceeded`);
85
- }
86
- }
87
-
88
- return result;
89
- }
90
-
91
- checkRedirect(config: HostConfig): { code: number; target: string } | null {
92
- if (!config.redirect || !config.redirect.enabled) {
93
- return null;
94
- }
95
-
96
- const { code, target } = config.redirect;
97
-
98
- if (![301, 302, 303, 307, 308].includes(code)) {
99
- return null;
100
- }
101
-
102
- try {
103
- const parsed = new URL(target);
104
- if (!parsed.protocol || !parsed.hostname) {
105
- return null;
106
- }
107
- } catch {
108
- return null;
109
- }
110
-
111
- return { code, target };
112
- }
113
-
114
- sanitizeHeader(value: string): string | null {
115
- if (typeof value !== "string") return null;
116
- if (/[\r\n\t]/.test(value)) return null;
117
- if (/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(value)) return null;
118
- if (/%0[dD]/i.test(value) || /%0[aA]/.test(value)) return null;
119
- if (value.length > 8192) return null;
120
- return value;
121
- }
122
-
123
- isValidHeaderName(name: string): boolean {
124
- if (typeof name !== "string" || name.length === 0) return false;
125
- if (name.length > 256) return false;
126
- return /^[a-zA-Z0-9!#$%&'*+\-.^_`|~]+$/.test(name);
127
- }
128
-
129
- getHopByHopHeaders(): string[] {
130
- return [
131
- "connection",
132
- "keep-alive",
133
- "proxy-authenticate",
134
- "proxy-authorization",
135
- "te",
136
- "trailer",
137
- "transfer-encoding",
138
- "upgrade",
139
- ];
140
- }
141
-
142
- calculateTimeout(config: HostConfig): number {
143
- if (config.http_proxy?.enabled) {
144
- return Math.max(1000, Math.min(30000, 5000));
145
- }
146
- return 0;
147
- }
148
- }