palaryn 0.1.0 → 0.3.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/README.md +243 -588
- package/dist/sdk/typescript/src/client.js +2 -2
- package/dist/sdk/typescript/src/client.js.map +1 -1
- package/dist/src/anomaly/detector.d.ts +7 -4
- package/dist/src/anomaly/detector.d.ts.map +1 -1
- package/dist/src/anomaly/detector.js +22 -12
- package/dist/src/anomaly/detector.js.map +1 -1
- package/dist/src/audit/logger.d.ts +10 -0
- package/dist/src/audit/logger.d.ts.map +1 -1
- package/dist/src/audit/logger.js +52 -38
- package/dist/src/audit/logger.js.map +1 -1
- package/dist/src/auth/routes.d.ts.map +1 -1
- package/dist/src/auth/routes.js +35 -0
- package/dist/src/auth/routes.js.map +1 -1
- package/dist/src/budget/manager.d.ts +5 -0
- package/dist/src/budget/manager.d.ts.map +1 -1
- package/dist/src/budget/manager.js +32 -0
- package/dist/src/budget/manager.js.map +1 -1
- package/dist/src/budget/model-pricing.d.ts +20 -0
- package/dist/src/budget/model-pricing.d.ts.map +1 -0
- package/dist/src/budget/model-pricing.js +107 -0
- package/dist/src/budget/model-pricing.js.map +1 -0
- package/dist/src/budget/usage-extractor.d.ts +3 -1
- package/dist/src/budget/usage-extractor.d.ts.map +1 -1
- package/dist/src/budget/usage-extractor.js +47 -3
- package/dist/src/budget/usage-extractor.js.map +1 -1
- package/dist/src/config/defaults.d.ts.map +1 -1
- package/dist/src/config/defaults.js +65 -13
- package/dist/src/config/defaults.js.map +1 -1
- package/dist/src/dlp/tool-patterns.d.ts +7 -0
- package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
- package/dist/src/dlp/tool-patterns.js +34 -0
- package/dist/src/dlp/tool-patterns.js.map +1 -0
- package/dist/src/executor/filesystem-executor.d.ts +28 -0
- package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
- package/dist/src/executor/filesystem-executor.js +192 -0
- package/dist/src/executor/filesystem-executor.js.map +1 -0
- package/dist/src/executor/http-executor.d.ts.map +1 -1
- package/dist/src/executor/http-executor.js +22 -2
- package/dist/src/executor/http-executor.js.map +1 -1
- package/dist/src/executor/index.d.ts +4 -0
- package/dist/src/executor/index.d.ts.map +1 -1
- package/dist/src/executor/index.js +9 -1
- package/dist/src/executor/index.js.map +1 -1
- package/dist/src/executor/shell-executor.d.ts +22 -0
- package/dist/src/executor/shell-executor.d.ts.map +1 -0
- package/dist/src/executor/shell-executor.js +119 -0
- package/dist/src/executor/shell-executor.js.map +1 -0
- package/dist/src/executor/sql-executor.d.ts +29 -0
- package/dist/src/executor/sql-executor.d.ts.map +1 -0
- package/dist/src/executor/sql-executor.js +114 -0
- package/dist/src/executor/sql-executor.js.map +1 -0
- package/dist/src/executor/websocket-executor.d.ts +26 -0
- package/dist/src/executor/websocket-executor.d.ts.map +1 -0
- package/dist/src/executor/websocket-executor.js +205 -0
- package/dist/src/executor/websocket-executor.js.map +1 -0
- package/dist/src/interceptor/index.d.ts +2 -0
- package/dist/src/interceptor/index.d.ts.map +1 -0
- package/dist/src/interceptor/index.js +6 -0
- package/dist/src/interceptor/index.js.map +1 -0
- package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
- package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
- package/dist/src/interceptor/provider-interceptor.js +302 -0
- package/dist/src/interceptor/provider-interceptor.js.map +1 -0
- package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
- package/dist/src/mcp/auth-verifier.js +3 -2
- package/dist/src/mcp/auth-verifier.js.map +1 -1
- package/dist/src/mcp/bridge.d.ts +14 -10
- package/dist/src/mcp/bridge.d.ts.map +1 -1
- package/dist/src/mcp/bridge.js +51 -227
- package/dist/src/mcp/bridge.js.map +1 -1
- package/dist/src/mcp/http-transport.d.ts +2 -0
- package/dist/src/mcp/http-transport.d.ts.map +1 -1
- package/dist/src/mcp/http-transport.js +117 -66
- package/dist/src/mcp/http-transport.js.map +1 -1
- package/dist/src/mcp/internal-auth.d.ts +13 -0
- package/dist/src/mcp/internal-auth.d.ts.map +1 -0
- package/dist/src/mcp/internal-auth.js +12 -0
- package/dist/src/mcp/internal-auth.js.map +1 -0
- package/dist/src/mcp/tool-definitions.d.ts +41 -0
- package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
- package/dist/src/mcp/tool-definitions.js +491 -0
- package/dist/src/mcp/tool-definitions.js.map +1 -0
- package/dist/src/middleware/auth.js.map +1 -1
- package/dist/src/middleware/session.js.map +1 -1
- package/dist/src/middleware/validate.d.ts +8 -0
- package/dist/src/middleware/validate.d.ts.map +1 -1
- package/dist/src/middleware/validate.js +45 -0
- package/dist/src/middleware/validate.js.map +1 -1
- package/dist/src/policy/engine.d.ts +4 -0
- package/dist/src/policy/engine.d.ts.map +1 -1
- package/dist/src/policy/engine.js +117 -0
- package/dist/src/policy/engine.js.map +1 -1
- package/dist/src/saas/routes.d.ts.map +1 -1
- package/dist/src/saas/routes.js +355 -22
- package/dist/src/saas/routes.js.map +1 -1
- package/dist/src/server/app.d.ts.map +1 -1
- package/dist/src/server/app.js +24 -3
- package/dist/src/server/app.js.map +1 -1
- package/dist/src/server/gateway.d.ts.map +1 -1
- package/dist/src/server/gateway.js +17 -0
- package/dist/src/server/gateway.js.map +1 -1
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/server/index.js +18 -0
- package/dist/src/server/index.js.map +1 -1
- package/dist/src/storage/interfaces.d.ts +14 -3
- package/dist/src/storage/interfaces.d.ts.map +1 -1
- package/dist/src/storage/memory.d.ts +2 -0
- package/dist/src/storage/memory.d.ts.map +1 -1
- package/dist/src/storage/memory.js +6 -0
- package/dist/src/storage/memory.js.map +1 -1
- package/dist/src/storage/postgres.d.ts +5 -0
- package/dist/src/storage/postgres.d.ts.map +1 -1
- package/dist/src/storage/postgres.js +16 -0
- package/dist/src/storage/postgres.js.map +1 -1
- package/dist/src/storage/redis.d.ts +10 -0
- package/dist/src/storage/redis.d.ts.map +1 -1
- package/dist/src/storage/redis.js +65 -0
- package/dist/src/storage/redis.js.map +1 -1
- package/dist/src/types/budget.d.ts +4 -0
- package/dist/src/types/budget.d.ts.map +1 -1
- package/dist/src/types/config.d.ts +58 -0
- package/dist/src/types/config.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +1 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/policy.d.ts +11 -1
- package/dist/src/types/policy.d.ts.map +1 -1
- package/dist/src/types/tool-result.d.ts +11 -0
- package/dist/src/types/tool-result.d.ts.map +1 -1
- package/dist/tests/unit/app-routes.test.d.ts +2 -0
- package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/app-routes.test.js +715 -0
- package/dist/tests/unit/app-routes.test.js.map +1 -0
- package/dist/tests/unit/audit-logger.test.js +105 -0
- package/dist/tests/unit/audit-logger.test.js.map +1 -1
- package/dist/tests/unit/auth-providers.test.d.ts +2 -0
- package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-providers.test.js +279 -0
- package/dist/tests/unit/auth-providers.test.js.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.js +993 -0
- package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
- package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
- package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-verifier.test.js +505 -0
- package/dist/tests/unit/auth-verifier.test.js.map +1 -0
- package/dist/tests/unit/billing-routes.test.d.ts +2 -0
- package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/billing-routes.test.js +432 -0
- package/dist/tests/unit/billing-routes.test.js.map +1 -0
- package/dist/tests/unit/config-defaults.test.d.ts +2 -0
- package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
- package/dist/tests/unit/config-defaults.test.js +119 -0
- package/dist/tests/unit/config-defaults.test.js.map +1 -0
- package/dist/tests/unit/defaults.test.js +0 -10
- package/dist/tests/unit/defaults.test.js.map +1 -1
- package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
- package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/filesystem-executor.test.js +280 -0
- package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
- package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
- package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/gateway-branches.test.js +1039 -0
- package/dist/tests/unit/gateway-branches.test.js.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.js +495 -0
- package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
- package/dist/tests/unit/logger.test.d.ts +2 -0
- package/dist/tests/unit/logger.test.d.ts.map +1 -0
- package/dist/tests/unit/logger.test.js +97 -0
- package/dist/tests/unit/logger.test.js.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
- package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
- package/dist/tests/unit/metrics.test.js +102 -0
- package/dist/tests/unit/metrics.test.js.map +1 -1
- package/dist/tests/unit/model-pricing.test.d.ts +2 -0
- package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
- package/dist/tests/unit/model-pricing.test.js +87 -0
- package/dist/tests/unit/model-pricing.test.js.map +1 -0
- package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
- package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
- package/dist/tests/unit/oauth-stores.test.js +260 -0
- package/dist/tests/unit/oauth-stores.test.js.map +1 -0
- package/dist/tests/unit/policy-engine.test.js +466 -0
- package/dist/tests/unit/policy-engine.test.js.map +1 -1
- package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
- package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
- package/dist/tests/unit/provider-interceptor.test.js +472 -0
- package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
- package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.js +332 -0
- package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.js +405 -0
- package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
- package/dist/tests/unit/saas-routes.test.js +3 -3
- package/dist/tests/unit/saas-routes.test.js.map +1 -1
- package/dist/tests/unit/shell-executor.test.d.ts +2 -0
- package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/shell-executor.test.js +145 -0
- package/dist/tests/unit/shell-executor.test.js.map +1 -0
- package/dist/tests/unit/sql-executor.test.d.ts +2 -0
- package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/sql-executor.test.js +177 -0
- package/dist/tests/unit/sql-executor.test.js.map +1 -0
- package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
- package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
- package/dist/tests/unit/stream-proxy.test.js +147 -0
- package/dist/tests/unit/stream-proxy.test.js.map +1 -0
- package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
- package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
- package/dist/tests/unit/tool-definitions.test.js +184 -0
- package/dist/tests/unit/tool-definitions.test.js.map +1 -0
- package/dist/tests/unit/usage-extractor.test.js +140 -0
- package/dist/tests/unit/usage-extractor.test.js.map +1 -1
- package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
- package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-handler.test.js +453 -0
- package/dist/tests/unit/webhook-handler.test.js.map +1 -0
- package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
- package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-routes.test.js +69 -0
- package/dist/tests/unit/webhook-routes.test.js.map +1 -0
- package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
- package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/websocket-executor.test.js +121 -0
- package/dist/tests/unit/websocket-executor.test.js.map +1 -0
- package/package.json +8 -2
- package/policy-packs/demo_fail.yaml +41 -0
- package/policy-packs/full_tools.yaml +136 -0
- package/src/admin/index.ts +1 -0
- package/src/admin/routes.ts +509 -0
- package/src/admin/templates.ts +572 -0
- package/src/anomaly/detector.ts +730 -0
- package/src/anomaly/index.ts +1 -0
- package/src/approval/manager.ts +569 -0
- package/src/approval/webhook.ts +133 -0
- package/src/audit/logger.ts +490 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/password.ts +21 -0
- package/src/auth/pkce.ts +22 -0
- package/src/auth/providers.ts +208 -0
- package/src/auth/routes.ts +561 -0
- package/src/auth/session.ts +84 -0
- package/src/billing/index.ts +6 -0
- package/src/billing/plan-enforcer.ts +135 -0
- package/src/billing/routes.ts +229 -0
- package/src/billing/stripe-client.ts +58 -0
- package/src/billing/webhook-handler.ts +182 -0
- package/src/billing/webhook-routes.ts +28 -0
- package/src/budget/manager.ts +679 -0
- package/src/budget/model-pricing.ts +119 -0
- package/src/budget/usage-extractor.ts +214 -0
- package/src/cli.ts +91 -0
- package/src/config/defaults.ts +261 -0
- package/src/config/validate.ts +88 -0
- package/src/dlp/composite-scanner.ts +213 -0
- package/src/dlp/index.ts +9 -0
- package/src/dlp/interfaces.ts +34 -0
- package/src/dlp/patterns.ts +30 -0
- package/src/dlp/prompt-injection-backend.ts +181 -0
- package/src/dlp/prompt-injection-patterns.ts +302 -0
- package/src/dlp/regex-backend.ts +181 -0
- package/src/dlp/scanner.ts +502 -0
- package/src/dlp/text-normalizer.ts +225 -0
- package/src/dlp/tool-patterns.ts +35 -0
- package/src/dlp/trufflehog-backend.ts +190 -0
- package/src/executor/filesystem-executor.ts +196 -0
- package/src/executor/http-executor.ts +349 -0
- package/src/executor/index.ts +9 -0
- package/src/executor/interfaces.ts +11 -0
- package/src/executor/noop-executor.ts +23 -0
- package/src/executor/registry.ts +64 -0
- package/src/executor/shell-executor.ts +148 -0
- package/src/executor/slack-executor.ts +176 -0
- package/src/executor/sql-executor.ts +146 -0
- package/src/executor/websocket-executor.ts +211 -0
- package/src/index.ts +24 -0
- package/src/interceptor/index.ts +1 -0
- package/src/interceptor/provider-interceptor.ts +315 -0
- package/src/mcp/auth-verifier.ts +152 -0
- package/src/mcp/bridge.ts +703 -0
- package/src/mcp/http-transport.ts +698 -0
- package/src/mcp/index.ts +9 -0
- package/src/mcp/internal-auth.ts +14 -0
- package/src/mcp/oauth-pages.ts +139 -0
- package/src/mcp/oauth-postgres-stores.ts +278 -0
- package/src/mcp/oauth-provider.ts +536 -0
- package/src/mcp/oauth-stores.ts +202 -0
- package/src/mcp/server.ts +55 -0
- package/src/mcp/tool-definitions.ts +562 -0
- package/src/metrics/collector.ts +357 -0
- package/src/metrics/index.ts +1 -0
- package/src/middleware/auth.ts +814 -0
- package/src/middleware/session.ts +85 -0
- package/src/middleware/validate.ts +130 -0
- package/src/policy/engine.ts +815 -0
- package/src/policy/index.ts +2 -0
- package/src/policy/opa-engine.ts +829 -0
- package/src/proxy/forward-proxy.ts +649 -0
- package/src/proxy/index.ts +1 -0
- package/src/ratelimit/limiter.ts +196 -0
- package/src/replay/engine.ts +142 -0
- package/src/replay/index.ts +1 -0
- package/src/saas/index.ts +1 -0
- package/src/saas/routes.ts +2178 -0
- package/src/server/app.ts +985 -0
- package/src/server/errors.ts +49 -0
- package/src/server/gateway.ts +1130 -0
- package/src/server/index.ts +307 -0
- package/src/server/logger.ts +255 -0
- package/src/server/stream-proxy.ts +202 -0
- package/src/storage/file-persistence.ts +315 -0
- package/src/storage/index.ts +4 -0
- package/src/storage/interfaces.ts +287 -0
- package/src/storage/memory.ts +686 -0
- package/src/storage/postgres.ts +1831 -0
- package/src/storage/redis.ts +835 -0
- package/src/tracing/index.ts +1 -0
- package/src/tracing/provider.ts +100 -0
- package/src/trust/calculator.ts +141 -0
- package/src/trust/index.ts +7 -0
- package/src/types/budget.ts +36 -0
- package/src/types/config.ts +278 -0
- package/src/types/events.ts +41 -0
- package/src/types/express.d.ts +14 -0
- package/src/types/index.ts +7 -0
- package/src/types/policy.ts +83 -0
- package/src/types/stripe-config.ts +11 -0
- package/src/types/subscription.ts +59 -0
- package/src/types/tool-call.ts +47 -0
- package/src/types/tool-result.ts +82 -0
- package/src/types/user.ts +125 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import * as https from 'https';
|
|
3
|
+
import * as net from 'net';
|
|
4
|
+
import * as dns from 'dns';
|
|
5
|
+
import { URL } from 'url';
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
import { Gateway, PreExecuteResult } from '../server/gateway';
|
|
8
|
+
import { GatewayConfig, ProxyConfig } from '../types/config';
|
|
9
|
+
import { ToolCall } from '../types/tool-call';
|
|
10
|
+
import { parseProxyAuth } from '../middleware/auth';
|
|
11
|
+
import { isPrivateIP } from '../executor/http-executor';
|
|
12
|
+
import { logger } from '../server/logger';
|
|
13
|
+
|
|
14
|
+
/** Maximum accumulated response body for post-DLP scanning (50 MB) */
|
|
15
|
+
const MAX_ACCUMULATED_BYTES = 50 * 1024 * 1024;
|
|
16
|
+
|
|
17
|
+
/** DNS lookup timeout in ms */
|
|
18
|
+
const DNS_LOOKUP_TIMEOUT_MS = 5000;
|
|
19
|
+
|
|
20
|
+
/** Maximum request body size in bytes (10 MB) — prevents OOM DoS */
|
|
21
|
+
const MAX_REQUEST_BODY_BYTES = 10 * 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
/** Global counter for total bytes being accumulated across all active proxy responses */
|
|
24
|
+
let globalAccumulatedBytes = 0;
|
|
25
|
+
/** Maximum total bytes accumulated across all concurrent proxy responses (500 MB) */
|
|
26
|
+
const MAX_GLOBAL_ACCUMULATED_BYTES = 500 * 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
/** Headers that must never be forwarded or logged */
|
|
29
|
+
const SENSITIVE_HEADERS = new Set([
|
|
30
|
+
'proxy-authorization', 'proxy-connection',
|
|
31
|
+
'x-palaryn-workspace', 'x-palaryn-actor',
|
|
32
|
+
'authorization', 'cookie', 'x-api-key',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/** Map HTTP method to tool capability */
|
|
36
|
+
function methodToCapability(method: string): 'read' | 'write' | 'delete' | 'admin' {
|
|
37
|
+
switch (method.toUpperCase()) {
|
|
38
|
+
case 'GET':
|
|
39
|
+
case 'HEAD':
|
|
40
|
+
case 'OPTIONS':
|
|
41
|
+
return 'read';
|
|
42
|
+
case 'DELETE':
|
|
43
|
+
return 'delete';
|
|
44
|
+
default:
|
|
45
|
+
return 'write';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Check if a hostname matches a domain pattern (supports leading wildcard) */
|
|
50
|
+
function matchesDomain(hostname: string, pattern: string): boolean {
|
|
51
|
+
// Strip null bytes and normalize to prevent injection attacks
|
|
52
|
+
const clean = hostname.replace(/\0/g, '').toLowerCase().trim();
|
|
53
|
+
const cleanPattern = pattern.replace(/\0/g, '').toLowerCase().trim();
|
|
54
|
+
if (!clean || !cleanPattern) return false;
|
|
55
|
+
if (cleanPattern === clean) return true;
|
|
56
|
+
if (cleanPattern.startsWith('*.')) {
|
|
57
|
+
const suffix = cleanPattern.slice(2);
|
|
58
|
+
return clean === suffix || clean.endsWith('.' + suffix);
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Check if a hostname matches any passthrough domain */
|
|
64
|
+
function isPassthrough(hostname: string, patterns: string[]): boolean {
|
|
65
|
+
return patterns.some(p => matchesDomain(hostname, p));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate resolved IP is not private (SSRF protection) with DNS timeout.
|
|
70
|
+
* Returns the first valid resolved IP address for DNS pinning — the caller
|
|
71
|
+
* should connect to this IP instead of the hostname to prevent DNS rebinding.
|
|
72
|
+
*/
|
|
73
|
+
export function validateResolvedIP(hostname: string): Promise<string> {
|
|
74
|
+
if (net.isIP(hostname)) {
|
|
75
|
+
if (isPrivateIP(hostname)) {
|
|
76
|
+
return Promise.reject(new Error(`SSRF blocked: IP ${hostname} is a private/reserved address`));
|
|
77
|
+
}
|
|
78
|
+
return Promise.resolve(hostname);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const dnsPromise = new Promise<string>((resolve, reject) => {
|
|
82
|
+
dns.lookup(hostname, { all: true }, (err, addresses) => {
|
|
83
|
+
if (err) return reject(err);
|
|
84
|
+
if (!addresses || addresses.length === 0) {
|
|
85
|
+
return reject(new Error(`DNS lookup failed: no addresses found for "${hostname}"`));
|
|
86
|
+
}
|
|
87
|
+
for (const addr of addresses) {
|
|
88
|
+
if (isPrivateIP(addr.address)) {
|
|
89
|
+
return reject(
|
|
90
|
+
new Error(`SSRF blocked: hostname "${hostname}" resolved to private IP ${addr.address}`)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Return the first resolved IP for DNS pinning
|
|
95
|
+
resolve(addresses[0].address);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
100
|
+
setTimeout(() => reject(new Error(`DNS lookup timeout for "${hostname}" after ${DNS_LOOKUP_TIMEOUT_MS}ms`)), DNS_LOOKUP_TIMEOUT_MS);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return Promise.race([dnsPromise, timeoutPromise]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Strip sensitive headers from a headers record */
|
|
107
|
+
function stripSensitiveHeaders(headers: Record<string, string>): Record<string, string> {
|
|
108
|
+
const result: Record<string, string> = {};
|
|
109
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
110
|
+
if (!SENSITIVE_HEADERS.has(key.toLowerCase())) {
|
|
111
|
+
result[key] = value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Build a synthetic ToolCall from an incoming proxy request */
|
|
118
|
+
export function buildToolCallFromProxy(
|
|
119
|
+
method: string,
|
|
120
|
+
targetUrl: string,
|
|
121
|
+
headers: Record<string, string>,
|
|
122
|
+
body: string | undefined,
|
|
123
|
+
workspaceId: string,
|
|
124
|
+
actorId: string,
|
|
125
|
+
): ToolCall {
|
|
126
|
+
const parsedUrl = new URL(targetUrl);
|
|
127
|
+
const toolName = `http.${method.toLowerCase()}`;
|
|
128
|
+
|
|
129
|
+
// Remove sensitive headers before forwarding
|
|
130
|
+
const forwardHeaders = stripSensitiveHeaders(headers);
|
|
131
|
+
|
|
132
|
+
// Parse body if it looks like JSON
|
|
133
|
+
let parsedBody: unknown = body;
|
|
134
|
+
if (body && typeof body === 'string') {
|
|
135
|
+
try {
|
|
136
|
+
parsedBody = JSON.parse(body);
|
|
137
|
+
} catch (_e) {
|
|
138
|
+
/* Not valid JSON — keep body as raw string */
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
tool_call_id: randomUUID(),
|
|
144
|
+
task_id: randomUUID(),
|
|
145
|
+
workspace_id: workspaceId,
|
|
146
|
+
actor: {
|
|
147
|
+
type: 'agent',
|
|
148
|
+
id: actorId,
|
|
149
|
+
},
|
|
150
|
+
source: {
|
|
151
|
+
platform: 'forward_proxy',
|
|
152
|
+
},
|
|
153
|
+
tool: {
|
|
154
|
+
name: toolName,
|
|
155
|
+
capability: methodToCapability(method),
|
|
156
|
+
},
|
|
157
|
+
args: {
|
|
158
|
+
method: method.toUpperCase(),
|
|
159
|
+
url: targetUrl,
|
|
160
|
+
headers: forwardHeaders,
|
|
161
|
+
body: parsedBody,
|
|
162
|
+
},
|
|
163
|
+
context: {
|
|
164
|
+
purpose: 'Proxied HTTP request',
|
|
165
|
+
labels: ['proxy', parsedUrl.hostname],
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Forward the request to the target and pipe the response back.
|
|
171
|
+
* When pinnedIP is provided (from SSRF DNS validation), the TCP connection
|
|
172
|
+
* goes to that IP while the Host header and TLS SNI use the original hostname,
|
|
173
|
+
* preventing DNS rebinding attacks.
|
|
174
|
+
*/
|
|
175
|
+
function forwardRequest(
|
|
176
|
+
method: string,
|
|
177
|
+
targetUrl: string,
|
|
178
|
+
headers: Record<string, string>,
|
|
179
|
+
body: string | undefined,
|
|
180
|
+
clientRes: http.ServerResponse,
|
|
181
|
+
gateway: Gateway,
|
|
182
|
+
toolCall: ToolCall,
|
|
183
|
+
pre: PreExecuteResult,
|
|
184
|
+
timeoutMs: number,
|
|
185
|
+
pinnedIP?: string,
|
|
186
|
+
): void {
|
|
187
|
+
const parsedUrl = new URL(targetUrl);
|
|
188
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
189
|
+
const transport = isHttps ? https : http;
|
|
190
|
+
|
|
191
|
+
// Strip sensitive headers, then set host
|
|
192
|
+
const forwardHeaders = stripSensitiveHeaders(headers);
|
|
193
|
+
forwardHeaders['host'] = parsedUrl.host;
|
|
194
|
+
|
|
195
|
+
const requestOptions: http.RequestOptions & { servername?: string } = {
|
|
196
|
+
// Use pinned IP for the actual connection to prevent DNS rebinding
|
|
197
|
+
hostname: pinnedIP || parsedUrl.hostname,
|
|
198
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
199
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
200
|
+
method: method.toUpperCase(),
|
|
201
|
+
headers: forwardHeaders,
|
|
202
|
+
timeout: timeoutMs,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// When using a pinned IP with HTTPS, set servername for correct TLS SNI
|
|
206
|
+
if (pinnedIP && isHttps) {
|
|
207
|
+
requestOptions.servername = parsedUrl.hostname;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Pin DNS resolution to the pre-validated IP to prevent TOCTOU rebinding
|
|
211
|
+
if (pinnedIP) {
|
|
212
|
+
const family = pinnedIP.includes(':') ? 6 : 4;
|
|
213
|
+
const agent = new (isHttps ? https : http).Agent({
|
|
214
|
+
lookup: (_hostname: string, _options: any, callback: Function) => {
|
|
215
|
+
callback(null, pinnedIP, family);
|
|
216
|
+
},
|
|
217
|
+
} as any);
|
|
218
|
+
requestOptions.agent = agent;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const proxyReq = transport.request(requestOptions, (upstreamRes) => {
|
|
222
|
+
const httpStatus = upstreamRes.statusCode || 502;
|
|
223
|
+
|
|
224
|
+
// Collect response headers
|
|
225
|
+
const responseHeaders: Record<string, string> = {};
|
|
226
|
+
for (const [key, value] of Object.entries(upstreamRes.headers)) {
|
|
227
|
+
if (value) responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Remove hop-by-hop headers
|
|
231
|
+
delete responseHeaders['transfer-encoding'];
|
|
232
|
+
delete responseHeaders['connection'];
|
|
233
|
+
delete responseHeaders['keep-alive'];
|
|
234
|
+
delete responseHeaders['proxy-authenticate'];
|
|
235
|
+
delete responseHeaders['proxy-authorization'];
|
|
236
|
+
delete responseHeaders['upgrade'];
|
|
237
|
+
|
|
238
|
+
// Write status and headers to client
|
|
239
|
+
clientRes.writeHead(httpStatus, responseHeaders);
|
|
240
|
+
|
|
241
|
+
// Accumulate response body for post-DLP
|
|
242
|
+
let accumulated = '';
|
|
243
|
+
let accumulatedBytes = 0;
|
|
244
|
+
let truncated = false;
|
|
245
|
+
|
|
246
|
+
upstreamRes.on('data', (chunk: Buffer) => {
|
|
247
|
+
clientRes.write(chunk);
|
|
248
|
+
accumulatedBytes += chunk.length;
|
|
249
|
+
globalAccumulatedBytes += chunk.length;
|
|
250
|
+
if (accumulatedBytes <= MAX_ACCUMULATED_BYTES && globalAccumulatedBytes <= MAX_GLOBAL_ACCUMULATED_BYTES) {
|
|
251
|
+
accumulated += chunk.toString();
|
|
252
|
+
} else if (!truncated) {
|
|
253
|
+
truncated = true;
|
|
254
|
+
const reason = globalAccumulatedBytes > MAX_GLOBAL_ACCUMULATED_BYTES
|
|
255
|
+
? `global accumulated bytes (${MAX_GLOBAL_ACCUMULATED_BYTES})`
|
|
256
|
+
: `per-request limit (${MAX_ACCUMULATED_BYTES})`;
|
|
257
|
+
logger.warn(`Response body truncated at ${reason} bytes for DLP scan`, { component: 'forward-proxy', target: targetUrl });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
upstreamRes.on('end', () => {
|
|
262
|
+
clientRes.end();
|
|
263
|
+
globalAccumulatedBytes = Math.max(0, globalAccumulatedBytes - accumulatedBytes);
|
|
264
|
+
|
|
265
|
+
// Run post-execute pipeline in background (DLP scan, budget recording, audit)
|
|
266
|
+
gateway.postExecute(toolCall, {
|
|
267
|
+
http_status: httpStatus,
|
|
268
|
+
body: accumulated,
|
|
269
|
+
headers: responseHeaders,
|
|
270
|
+
}, pre).catch(err => {
|
|
271
|
+
logger.error('postExecute failed', { component: 'forward-proxy', error: err instanceof Error ? err.message : String(err) });
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
upstreamRes.on('error', (err) => {
|
|
276
|
+
// Release accumulated bytes to prevent counter leak on error
|
|
277
|
+
globalAccumulatedBytes = Math.max(0, globalAccumulatedBytes - accumulatedBytes);
|
|
278
|
+
if (!clientRes.headersSent) {
|
|
279
|
+
clientRes.writeHead(502, { 'Content-Type': 'application/json' });
|
|
280
|
+
}
|
|
281
|
+
clientRes.end(JSON.stringify({ error: `Upstream error: ${err.message}` }));
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
proxyReq.on('error', (err) => {
|
|
286
|
+
if (!clientRes.headersSent) {
|
|
287
|
+
clientRes.writeHead(502, { 'Content-Type': 'application/json' });
|
|
288
|
+
}
|
|
289
|
+
clientRes.end(JSON.stringify({ error: `Proxy connection failed: ${err.message}` }));
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
proxyReq.on('timeout', () => {
|
|
293
|
+
proxyReq.destroy();
|
|
294
|
+
if (!clientRes.headersSent) {
|
|
295
|
+
clientRes.writeHead(504, { 'Content-Type': 'application/json' });
|
|
296
|
+
}
|
|
297
|
+
clientRes.end(JSON.stringify({ error: `Upstream request timeout after ${timeoutMs}ms` }));
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Handle client disconnect
|
|
301
|
+
clientRes.on('close', () => {
|
|
302
|
+
if (!proxyReq.destroyed) {
|
|
303
|
+
proxyReq.destroy();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (body) {
|
|
308
|
+
proxyReq.write(body);
|
|
309
|
+
}
|
|
310
|
+
proxyReq.end();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Handle a CONNECT tunnel request (HTTPS).
|
|
315
|
+
* Phase 1: Domain-level policy only — no content inspection.
|
|
316
|
+
* Builds a synthetic ToolCall for policy evaluation, then either
|
|
317
|
+
* establishes a TCP tunnel or returns 403.
|
|
318
|
+
*/
|
|
319
|
+
async function handleConnect(
|
|
320
|
+
req: http.IncomingMessage,
|
|
321
|
+
socket: net.Socket,
|
|
322
|
+
head: Buffer,
|
|
323
|
+
gateway: Gateway,
|
|
324
|
+
config: GatewayConfig,
|
|
325
|
+
proxyConfig: ProxyConfig,
|
|
326
|
+
): Promise<void> {
|
|
327
|
+
const target = req.url || '';
|
|
328
|
+
const [hostname, portStr] = target.split(':');
|
|
329
|
+
const port = parseInt(portStr, 10) || 443;
|
|
330
|
+
|
|
331
|
+
// Auth
|
|
332
|
+
const headers: Record<string, string> = {};
|
|
333
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
334
|
+
if (typeof value === 'string') headers[key] = value;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const auth = parseProxyAuth(headers, proxyConfig, config.auth);
|
|
338
|
+
if (!auth.authenticated) {
|
|
339
|
+
socket.write('HTTP/1.1 407 Proxy Authentication Required\r\n');
|
|
340
|
+
socket.write('Proxy-Authenticate: Basic realm="Palaryn Proxy"\r\n');
|
|
341
|
+
socket.write('Content-Type: application/json\r\n\r\n');
|
|
342
|
+
socket.write(JSON.stringify({ error: auth.error }));
|
|
343
|
+
socket.end();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Passthrough domains skip policy entirely
|
|
348
|
+
if (proxyConfig.passthrough_domains && isPassthrough(hostname, proxyConfig.passthrough_domains)) {
|
|
349
|
+
return establishTunnel(hostname, port, socket, head);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Build a synthetic ToolCall for domain-level policy evaluation
|
|
353
|
+
const toolCall = buildToolCallFromProxy(
|
|
354
|
+
'CONNECT',
|
|
355
|
+
`https://${hostname}:${port}`,
|
|
356
|
+
headers,
|
|
357
|
+
undefined,
|
|
358
|
+
auth.workspace_id!,
|
|
359
|
+
auth.actor_id!,
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// Run pre-execute pipeline (policy, rate limit, etc.)
|
|
363
|
+
try {
|
|
364
|
+
const pre = await gateway.preExecute(toolCall);
|
|
365
|
+
if (!pre.allowed) {
|
|
366
|
+
const reason = pre.result?.error || pre.result?.policy?.reasons?.join(', ') || 'Blocked by policy';
|
|
367
|
+
socket.write('HTTP/1.1 403 Forbidden\r\n');
|
|
368
|
+
socket.write('Content-Type: application/json\r\n\r\n');
|
|
369
|
+
socket.write(JSON.stringify({ error: reason }));
|
|
370
|
+
socket.end();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
} catch (err) {
|
|
374
|
+
const message = err instanceof Error ? err.message : 'Policy evaluation failed';
|
|
375
|
+
socket.write('HTTP/1.1 500 Internal Server Error\r\n');
|
|
376
|
+
socket.write('Content-Type: application/json\r\n\r\n');
|
|
377
|
+
socket.write(JSON.stringify({ error: message }));
|
|
378
|
+
socket.end();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// SSRF protection with DNS pinning
|
|
383
|
+
let connectHost = hostname;
|
|
384
|
+
if (proxyConfig.ssrf_protection !== false) {
|
|
385
|
+
try {
|
|
386
|
+
connectHost = await validateResolvedIP(hostname);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
const message = err instanceof Error ? err.message : 'SSRF blocked';
|
|
389
|
+
socket.write('HTTP/1.1 403 Forbidden\r\n');
|
|
390
|
+
socket.write('Content-Type: application/json\r\n\r\n');
|
|
391
|
+
socket.write(JSON.stringify({ error: message }));
|
|
392
|
+
socket.end();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return establishTunnel(connectHost, port, socket, head);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Establish a raw TCP tunnel for CONNECT requests */
|
|
401
|
+
function establishTunnel(
|
|
402
|
+
hostname: string,
|
|
403
|
+
port: number,
|
|
404
|
+
clientSocket: net.Socket,
|
|
405
|
+
head: Buffer,
|
|
406
|
+
): Promise<void> {
|
|
407
|
+
return new Promise((resolve, reject) => {
|
|
408
|
+
const upstream = net.connect(port, hostname, () => {
|
|
409
|
+
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
410
|
+
if (head.length > 0) {
|
|
411
|
+
upstream.write(head);
|
|
412
|
+
}
|
|
413
|
+
upstream.pipe(clientSocket);
|
|
414
|
+
clientSocket.pipe(upstream);
|
|
415
|
+
resolve();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
upstream.on('error', (err) => {
|
|
419
|
+
clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
|
|
420
|
+
clientSocket.end();
|
|
421
|
+
resolve(); // Don't reject — just close the tunnel
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
clientSocket.on('error', () => {
|
|
425
|
+
upstream.destroy();
|
|
426
|
+
resolve();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Timeout for tunnel establishment
|
|
430
|
+
upstream.setTimeout(30000, () => {
|
|
431
|
+
upstream.destroy();
|
|
432
|
+
clientSocket.write('HTTP/1.1 504 Gateway Timeout\r\n\r\n');
|
|
433
|
+
clientSocket.end();
|
|
434
|
+
resolve();
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export interface ForwardProxyServer {
|
|
440
|
+
server: http.Server;
|
|
441
|
+
listen: (port: number, callback?: () => void) => http.Server;
|
|
442
|
+
close: (callback?: (err?: Error) => void) => http.Server;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Create a forward HTTP proxy server that routes all requests through
|
|
447
|
+
* the Palaryn gateway pipeline (policy, DLP, budget, audit).
|
|
448
|
+
*
|
|
449
|
+
* Usage:
|
|
450
|
+
* const proxy = createForwardProxy(gateway, config);
|
|
451
|
+
* proxy.listen(3128);
|
|
452
|
+
*
|
|
453
|
+
* Clients configure:
|
|
454
|
+
* HTTP_PROXY=http://workspace:apikey@localhost:3128
|
|
455
|
+
*/
|
|
456
|
+
export function createForwardProxy(
|
|
457
|
+
gateway: Gateway,
|
|
458
|
+
config: GatewayConfig,
|
|
459
|
+
): ForwardProxyServer {
|
|
460
|
+
const proxyConfig = config.proxy || {
|
|
461
|
+
enabled: true,
|
|
462
|
+
port: 3128,
|
|
463
|
+
require_auth: true,
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const timeoutMs = config.executor?.http?.timeout_ms || 15000;
|
|
467
|
+
const ssrfEnabled = proxyConfig.ssrf_protection !== false; // default: true
|
|
468
|
+
|
|
469
|
+
const server = http.createServer(async (req, res) => {
|
|
470
|
+
try {
|
|
471
|
+
// Proxy requests have absolute URLs: GET http://example.com/path HTTP/1.1
|
|
472
|
+
const targetUrl = req.url || '';
|
|
473
|
+
|
|
474
|
+
// Non-proxy requests (relative URL) get a 400
|
|
475
|
+
if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
|
|
476
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
477
|
+
res.end(JSON.stringify({
|
|
478
|
+
error: 'Not a proxy request. Use this server as an HTTP proxy (HTTP_PROXY env var).',
|
|
479
|
+
}));
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
484
|
+
const parsedUrl = new URL(targetUrl);
|
|
485
|
+
|
|
486
|
+
// Extract headers as flat record
|
|
487
|
+
const headers: Record<string, string> = {};
|
|
488
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
489
|
+
if (typeof value === 'string') headers[key] = value;
|
|
490
|
+
else if (Array.isArray(value)) headers[key] = value.join(', ');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Auth
|
|
494
|
+
const auth = parseProxyAuth(headers, proxyConfig, config.auth);
|
|
495
|
+
if (!auth.authenticated) {
|
|
496
|
+
res.writeHead(407, {
|
|
497
|
+
'Content-Type': 'application/json',
|
|
498
|
+
'Proxy-Authenticate': 'Basic realm="Palaryn Proxy"',
|
|
499
|
+
});
|
|
500
|
+
res.end(JSON.stringify({ error: auth.error }));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Passthrough domains skip policy eval and DLP, but still run rate limiting and audit
|
|
505
|
+
if (proxyConfig.passthrough_domains && isPassthrough(parsedUrl.hostname, proxyConfig.passthrough_domains)) {
|
|
506
|
+
const bodyChunks: Buffer[] = [];
|
|
507
|
+
let requestBodyBytes = 0;
|
|
508
|
+
let bodyAborted = false;
|
|
509
|
+
req.on('data', (chunk: Buffer) => {
|
|
510
|
+
if (bodyAborted) return;
|
|
511
|
+
requestBodyBytes += chunk.length;
|
|
512
|
+
if (requestBodyBytes > MAX_REQUEST_BODY_BYTES) {
|
|
513
|
+
bodyAborted = true;
|
|
514
|
+
req.destroy();
|
|
515
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
516
|
+
res.end(JSON.stringify({ error: 'Request body too large', error_code: 'VALIDATION_FAILED' }));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
bodyChunks.push(chunk);
|
|
520
|
+
});
|
|
521
|
+
req.on('end', async () => {
|
|
522
|
+
if (bodyAborted) return;
|
|
523
|
+
const body = bodyChunks.length > 0 ? Buffer.concat(bodyChunks).toString() : undefined;
|
|
524
|
+
const toolCall = buildToolCallFromProxy(method, targetUrl, headers, body, auth.workspace_id!, auth.actor_id!);
|
|
525
|
+
|
|
526
|
+
// Rate limit check even for passthrough domains
|
|
527
|
+
const rateLimiter = gateway.getRateLimiter();
|
|
528
|
+
const rateLimitResult = rateLimiter.check(toolCall);
|
|
529
|
+
if (!rateLimitResult.allowed) {
|
|
530
|
+
res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
531
|
+
res.end(JSON.stringify({
|
|
532
|
+
error: `Rate limit exceeded (${rateLimitResult.blocked_by}): ${rateLimitResult.current}/${rateLimitResult.limit} requests in window`,
|
|
533
|
+
error_code: 'RATE_LIMIT_EXCEEDED',
|
|
534
|
+
}));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Audit logging for passthrough domains
|
|
539
|
+
const auditLogger = gateway.getAuditLogger();
|
|
540
|
+
auditLogger.logToolCallReceived(toolCall);
|
|
541
|
+
|
|
542
|
+
const pre: PreExecuteResult = { allowed: true, stepTimings: {}, startTime: Date.now() };
|
|
543
|
+
forwardRequest(method, targetUrl, headers, body, res, gateway, toolCall, pre, timeoutMs);
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Read request body with size limit
|
|
549
|
+
const bodyChunks: Buffer[] = [];
|
|
550
|
+
let requestBodyBytes = 0;
|
|
551
|
+
let bodyAborted = false;
|
|
552
|
+
req.on('data', (chunk: Buffer) => {
|
|
553
|
+
if (bodyAborted) return;
|
|
554
|
+
requestBodyBytes += chunk.length;
|
|
555
|
+
if (requestBodyBytes > MAX_REQUEST_BODY_BYTES) {
|
|
556
|
+
bodyAborted = true;
|
|
557
|
+
req.destroy();
|
|
558
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
559
|
+
res.end(JSON.stringify({ error: 'Request body too large', error_code: 'VALIDATION_FAILED' }));
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
bodyChunks.push(chunk);
|
|
563
|
+
});
|
|
564
|
+
req.on('end', async () => {
|
|
565
|
+
if (bodyAborted) return;
|
|
566
|
+
const body = bodyChunks.length > 0 ? Buffer.concat(bodyChunks).toString() : undefined;
|
|
567
|
+
|
|
568
|
+
// Build synthetic ToolCall
|
|
569
|
+
const toolCall = buildToolCallFromProxy(
|
|
570
|
+
method,
|
|
571
|
+
targetUrl,
|
|
572
|
+
headers,
|
|
573
|
+
body,
|
|
574
|
+
auth.workspace_id!,
|
|
575
|
+
auth.actor_id!,
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
// SSRF protection with DNS pinning
|
|
579
|
+
let pinnedIP: string | undefined;
|
|
580
|
+
if (ssrfEnabled) {
|
|
581
|
+
try {
|
|
582
|
+
pinnedIP = await validateResolvedIP(parsedUrl.hostname);
|
|
583
|
+
} catch (err) {
|
|
584
|
+
const message = err instanceof Error ? err.message : 'SSRF blocked';
|
|
585
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
586
|
+
res.end(JSON.stringify({ error: message }));
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Run pre-execute pipeline (rate limit, anomaly, policy, DLP, budget)
|
|
592
|
+
try {
|
|
593
|
+
const pre = await gateway.preExecute(toolCall);
|
|
594
|
+
|
|
595
|
+
if (!pre.allowed) {
|
|
596
|
+
const status = pre.result?.status === 'needs_approval' ? 202 : 403;
|
|
597
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
598
|
+
res.end(JSON.stringify(pre.result));
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Forward the request to the target
|
|
603
|
+
// Use the processed tool call args (may have been transformed by policy)
|
|
604
|
+
const forwardBody = pre.processedToolCall
|
|
605
|
+
? (typeof pre.processedToolCall.args.body === 'string'
|
|
606
|
+
? pre.processedToolCall.args.body
|
|
607
|
+
: pre.processedToolCall.args.body ? JSON.stringify(pre.processedToolCall.args.body) : body)
|
|
608
|
+
: body;
|
|
609
|
+
const forwardHeaders = pre.processedToolCall?.args.headers as Record<string, string> || headers;
|
|
610
|
+
|
|
611
|
+
forwardRequest(method, targetUrl, forwardHeaders, forwardBody, res, gateway, toolCall, pre, timeoutMs, pinnedIP);
|
|
612
|
+
|
|
613
|
+
} catch (err) {
|
|
614
|
+
const message = err instanceof Error ? err.message : 'Internal proxy error';
|
|
615
|
+
if (!res.headersSent) {
|
|
616
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
617
|
+
res.end(JSON.stringify({ error: message }));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
} catch (err) {
|
|
623
|
+
const message = err instanceof Error ? err.message : 'Internal proxy error';
|
|
624
|
+
if (!res.headersSent) {
|
|
625
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
626
|
+
res.end(JSON.stringify({ error: message }));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// Handle CONNECT method for HTTPS tunneling
|
|
632
|
+
server.on('connect', (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
|
|
633
|
+
handleConnect(req, socket, head, gateway, config, proxyConfig).catch(err => {
|
|
634
|
+
logger.error('CONNECT handler error', { component: 'forward-proxy', error: err instanceof Error ? err.message : String(err) });
|
|
635
|
+
try {
|
|
636
|
+
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
|
|
637
|
+
socket.end();
|
|
638
|
+
} catch (e) {
|
|
639
|
+
/* Socket already closed — expected when client disconnects */
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
server,
|
|
646
|
+
listen: (port: number, callback?: () => void) => server.listen(port, callback),
|
|
647
|
+
close: (callback?: (err?: Error) => void) => server.close(callback),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createForwardProxy, buildToolCallFromProxy, validateResolvedIP, ForwardProxyServer } from './forward-proxy';
|