@openape/proxy 0.4.2 → 0.4.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.
- package/README.md +10 -0
- package/dist/index.cjs +560 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +538 -138
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,14 +1,107 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
4
5
|
import { createServer } from "http";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join } from "path";
|
|
5
8
|
import { parseArgs } from "util";
|
|
6
9
|
|
|
10
|
+
// src/ca-store.ts
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
12
|
+
import { dirname } from "path";
|
|
13
|
+
import forge from "node-forge";
|
|
14
|
+
function loadOrCreateCa(opts) {
|
|
15
|
+
if (existsSync(opts.certPath) && existsSync(opts.keyPath)) {
|
|
16
|
+
return {
|
|
17
|
+
certPem: readFileSync(opts.certPath, "utf-8"),
|
|
18
|
+
keyPem: readFileSync(opts.keyPath, "utf-8"),
|
|
19
|
+
created: false
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 });
|
|
23
|
+
const cert = forge.pki.createCertificate();
|
|
24
|
+
cert.publicKey = keys.publicKey;
|
|
25
|
+
cert.serialNumber = String(Date.now());
|
|
26
|
+
cert.validity.notBefore = /* @__PURE__ */ new Date();
|
|
27
|
+
cert.validity.notAfter = new Date(Date.now() + 10 * 365 * 864e5);
|
|
28
|
+
const attrs = [
|
|
29
|
+
// @types/node-forge types valueTagClass as asn1.Class, but node-forge's
|
|
30
|
+
// x509.js reads it as an asn1.Type at runtime (see comment above). Cast to
|
|
31
|
+
// satisfy the too-narrow typing without changing runtime behaviour.
|
|
32
|
+
{ name: "commonName", value: opts.subjectCN, valueTagClass: forge.asn1.Type.UTF8 }
|
|
33
|
+
];
|
|
34
|
+
cert.setSubject(attrs);
|
|
35
|
+
cert.setIssuer(attrs);
|
|
36
|
+
cert.setExtensions([
|
|
37
|
+
{ name: "basicConstraints", cA: true },
|
|
38
|
+
{ name: "keyUsage", keyCertSign: true, cRLSign: true },
|
|
39
|
+
{ name: "subjectKeyIdentifier" }
|
|
40
|
+
]);
|
|
41
|
+
cert.sign(keys.privateKey, forge.md.sha256.create());
|
|
42
|
+
const certPem = forge.pki.certificateToPem(cert);
|
|
43
|
+
const keyPem = forge.pki.privateKeyToPem(keys.privateKey);
|
|
44
|
+
mkdirSync(dirname(opts.certPath), { recursive: true, mode: 448 });
|
|
45
|
+
writeFileSync(opts.certPath, certPem, { mode: 384 });
|
|
46
|
+
writeFileSync(opts.keyPath, keyPem, { mode: 384 });
|
|
47
|
+
return { certPem, keyPem, created: true };
|
|
48
|
+
}
|
|
49
|
+
function mintLeafCert(ca, hostname) {
|
|
50
|
+
const caCert = forge.pki.certificateFromPem(ca.certPem);
|
|
51
|
+
const caKey = forge.pki.privateKeyFromPem(ca.keyPem);
|
|
52
|
+
const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 });
|
|
53
|
+
const cert = forge.pki.createCertificate();
|
|
54
|
+
cert.publicKey = keys.publicKey;
|
|
55
|
+
cert.serialNumber = `${Date.now()}${Math.floor(Math.random() * 1e6)}`;
|
|
56
|
+
cert.validity.notBefore = /* @__PURE__ */ new Date();
|
|
57
|
+
const expiresAt = Date.now() + 24 * 36e5;
|
|
58
|
+
cert.validity.notAfter = new Date(expiresAt);
|
|
59
|
+
cert.setSubject([
|
|
60
|
+
// See note in createCaCert: valueTagClass is an asn1.Type at runtime;
|
|
61
|
+
// cast around the too-narrow @types/node-forge annotation.
|
|
62
|
+
{ name: "commonName", value: hostname, valueTagClass: forge.asn1.Type.UTF8 }
|
|
63
|
+
]);
|
|
64
|
+
cert.setIssuer(caCert.subject.attributes);
|
|
65
|
+
cert.setExtensions([
|
|
66
|
+
{ name: "basicConstraints", cA: false },
|
|
67
|
+
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true },
|
|
68
|
+
{ name: "extKeyUsage", serverAuth: true },
|
|
69
|
+
{ name: "subjectAltName", altNames: [{ type: 2, value: hostname }] }
|
|
70
|
+
]);
|
|
71
|
+
cert.sign(caKey, forge.md.sha256.create());
|
|
72
|
+
return {
|
|
73
|
+
certPem: forge.pki.certificateToPem(cert),
|
|
74
|
+
keyPem: forge.pki.privateKeyToPem(keys.privateKey),
|
|
75
|
+
expiresAt
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function createLeafCertCache(ca, opts) {
|
|
79
|
+
const cache = /* @__PURE__ */ new Map();
|
|
80
|
+
return {
|
|
81
|
+
get(hostname) {
|
|
82
|
+
const existing = cache.get(hostname);
|
|
83
|
+
if (existing && existing.expiresAt > Date.now()) {
|
|
84
|
+
cache.delete(hostname);
|
|
85
|
+
cache.set(hostname, existing);
|
|
86
|
+
return existing;
|
|
87
|
+
}
|
|
88
|
+
const fresh = mintLeafCert(ca, hostname);
|
|
89
|
+
cache.set(hostname, fresh);
|
|
90
|
+
while (cache.size > opts.capacity) {
|
|
91
|
+
const oldestKey = cache.keys().next().value;
|
|
92
|
+
if (oldestKey === void 0) break;
|
|
93
|
+
cache.delete(oldestKey);
|
|
94
|
+
}
|
|
95
|
+
return fresh;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
7
100
|
// src/config.ts
|
|
8
|
-
import { readFileSync } from "fs";
|
|
101
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
9
102
|
import { parse as parseTOML } from "smol-toml";
|
|
10
103
|
function loadMultiAgentConfig(path, overrides) {
|
|
11
|
-
const raw =
|
|
104
|
+
const raw = readFileSync2(path, "utf-8");
|
|
12
105
|
let parsed;
|
|
13
106
|
if (path.endsWith(".json")) {
|
|
14
107
|
parsed = JSON.parse(raw);
|
|
@@ -67,26 +160,26 @@ function matchesRule(rule, domain, method, path) {
|
|
|
67
160
|
}
|
|
68
161
|
return true;
|
|
69
162
|
}
|
|
70
|
-
function evaluateRules(
|
|
71
|
-
for (const rule of
|
|
163
|
+
function evaluateRules(config, domain, method, path) {
|
|
164
|
+
for (const rule of config.deny) {
|
|
72
165
|
if (matchesRule(rule, domain, method, path)) {
|
|
73
166
|
return { type: "deny", note: rule.note };
|
|
74
167
|
}
|
|
75
168
|
}
|
|
76
|
-
for (const rule of
|
|
169
|
+
for (const rule of config.allow) {
|
|
77
170
|
if (matchesRule(rule, domain, method, path)) {
|
|
78
171
|
return { type: "allow" };
|
|
79
172
|
}
|
|
80
173
|
}
|
|
81
|
-
for (const rule of
|
|
174
|
+
for (const rule of config.grant_required) {
|
|
82
175
|
if (matchesRule(rule, domain, method, path)) {
|
|
83
176
|
return { type: "grant_required", rule };
|
|
84
177
|
}
|
|
85
178
|
}
|
|
86
|
-
if (
|
|
179
|
+
if (config.proxy.default_action === "block") {
|
|
87
180
|
return { type: "deny", note: "No matching rule (default: block)" };
|
|
88
181
|
}
|
|
89
|
-
if (
|
|
182
|
+
if (config.proxy.default_action === "allow") {
|
|
90
183
|
return { type: "allow" };
|
|
91
184
|
}
|
|
92
185
|
return {
|
|
@@ -276,14 +369,14 @@ function isPrivateIP(ip) {
|
|
|
276
369
|
if (isIP(ip) === 6) return isPrivateIPv6(ip);
|
|
277
370
|
return false;
|
|
278
371
|
}
|
|
279
|
-
async function checkEgress(
|
|
280
|
-
if (isIP(
|
|
281
|
-
return isPrivateIP(
|
|
372
|
+
async function checkEgress(hostname) {
|
|
373
|
+
if (isIP(hostname)) {
|
|
374
|
+
return isPrivateIP(hostname) ? { kind: "private" } : { kind: "ok" };
|
|
282
375
|
}
|
|
283
|
-
if (
|
|
376
|
+
if (hostname === "localhost") return { kind: "private" };
|
|
284
377
|
let settled;
|
|
285
378
|
try {
|
|
286
|
-
settled = await Promise.allSettled([resolve4(
|
|
379
|
+
settled = await Promise.allSettled([resolve4(hostname), resolve6(hostname)]);
|
|
287
380
|
} catch {
|
|
288
381
|
return { kind: "unresolvable", reason: "dns-error" };
|
|
289
382
|
}
|
|
@@ -299,17 +392,100 @@ async function checkEgress(hostname2) {
|
|
|
299
392
|
|
|
300
393
|
// src/connect.ts
|
|
301
394
|
import { connect } from "net";
|
|
302
|
-
|
|
395
|
+
|
|
396
|
+
// src/mitm-connect.ts
|
|
397
|
+
import { TLSSocket, createSecureContext, connect as tlsConnect } from "tls";
|
|
398
|
+
function handleMitmConnect(opts) {
|
|
399
|
+
const leaf = opts.leafCache.get(opts.host);
|
|
400
|
+
const ctx = createSecureContext({ cert: leaf.certPem, key: leaf.keyPem });
|
|
401
|
+
const tls = new TLSSocket(opts.clientSocket, { isServer: true, secureContext: ctx });
|
|
402
|
+
opts.clientSocket.resume();
|
|
403
|
+
let buffer = Buffer.alloc(0);
|
|
404
|
+
let dispatched = false;
|
|
405
|
+
const onData = (chunk) => {
|
|
406
|
+
if (dispatched) return;
|
|
407
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
408
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
409
|
+
if (headerEnd < 0) return;
|
|
410
|
+
const headerBlock = buffer.subarray(0, headerEnd).toString("utf-8");
|
|
411
|
+
const lines = headerBlock.split("\r\n");
|
|
412
|
+
const requestLine = lines[0] ?? "";
|
|
413
|
+
const [method, path] = requestLine.split(" ");
|
|
414
|
+
const headers = /* @__PURE__ */ new Map();
|
|
415
|
+
for (const line of lines.slice(1)) {
|
|
416
|
+
const i = line.indexOf(":");
|
|
417
|
+
if (i > 0) headers.set(line.slice(0, i).trim().toLowerCase(), line.slice(i + 1).trim());
|
|
418
|
+
}
|
|
419
|
+
const decision = opts.onRequest({
|
|
420
|
+
method: method ?? "",
|
|
421
|
+
host: opts.host,
|
|
422
|
+
path: path ?? "/",
|
|
423
|
+
headers,
|
|
424
|
+
// inject decisions use only target+headers; raw body bytes still flow through to upstream below
|
|
425
|
+
body: null
|
|
426
|
+
});
|
|
427
|
+
if (decision.type === "short-circuit") {
|
|
428
|
+
dispatched = true;
|
|
429
|
+
const body = Buffer.from(decision.body, "utf-8");
|
|
430
|
+
tls.write(`HTTP/1.1 ${decision.status} OK\r
|
|
431
|
+
Content-Length: ${body.length}\r
|
|
432
|
+
\r
|
|
433
|
+
`);
|
|
434
|
+
tls.write(body);
|
|
435
|
+
tls.end();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
dispatched = true;
|
|
439
|
+
tls.pause();
|
|
440
|
+
const upstream = tlsConnect({
|
|
441
|
+
host: opts.host,
|
|
442
|
+
port: opts.port,
|
|
443
|
+
servername: opts.host,
|
|
444
|
+
rejectUnauthorized: opts.upstreamRejectUnauthorized ?? true
|
|
445
|
+
});
|
|
446
|
+
upstream.once("secureConnect", () => {
|
|
447
|
+
const mutated = decision.mutatedHeaders;
|
|
448
|
+
const newHeaderLines = [];
|
|
449
|
+
for (const line of lines.slice(1)) {
|
|
450
|
+
const i = line.indexOf(":");
|
|
451
|
+
if (i <= 0) continue;
|
|
452
|
+
const lower = line.slice(0, i).trim().toLowerCase();
|
|
453
|
+
if (mutated.has(lower)) continue;
|
|
454
|
+
newHeaderLines.push(line);
|
|
455
|
+
}
|
|
456
|
+
for (const [name, value] of mutated.entries()) {
|
|
457
|
+
newHeaderLines.push(`${capitalizeHeader(name)}: ${value}`);
|
|
458
|
+
}
|
|
459
|
+
const fullRequest = [requestLine, ...newHeaderLines, "", ""].join("\r\n");
|
|
460
|
+
upstream.write(fullRequest);
|
|
461
|
+
const remainder = buffer.subarray(headerEnd + 4);
|
|
462
|
+
if (remainder.length > 0) upstream.write(remainder);
|
|
463
|
+
upstream.pipe(tls);
|
|
464
|
+
tls.removeListener("data", onData);
|
|
465
|
+
tls.pipe(upstream);
|
|
466
|
+
tls.resume();
|
|
467
|
+
});
|
|
468
|
+
upstream.on("error", () => tls.end());
|
|
469
|
+
};
|
|
470
|
+
tls.on("data", onData);
|
|
471
|
+
tls.on("error", () => opts.clientSocket.destroy());
|
|
472
|
+
}
|
|
473
|
+
function capitalizeHeader(name) {
|
|
474
|
+
return name.split("-").map((p) => (p[0]?.toUpperCase() ?? "") + p.slice(1)).join("-");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/connect.ts
|
|
478
|
+
async function handleConnect(config, grantsClients, req, clientSocket, head, deps) {
|
|
303
479
|
const target = req.url ?? "";
|
|
304
480
|
const [host, portStr] = target.split(":");
|
|
305
|
-
const
|
|
306
|
-
if (!host || !
|
|
481
|
+
const port = Number.parseInt(portStr || "443");
|
|
482
|
+
if (!host || !port) {
|
|
307
483
|
clientSocket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
308
484
|
clientSocket.destroy();
|
|
309
485
|
return;
|
|
310
486
|
}
|
|
311
487
|
const idpHosts = new Set(
|
|
312
|
-
|
|
488
|
+
config.agents.map((a) => {
|
|
313
489
|
try {
|
|
314
490
|
return new URL(a.idp_url).hostname.toLowerCase();
|
|
315
491
|
} catch {
|
|
@@ -327,46 +503,51 @@ async function handleConnect(config2, grantsClients, req, clientSocket, _head) {
|
|
|
327
503
|
path: target,
|
|
328
504
|
rule: "idp-system-bypass"
|
|
329
505
|
});
|
|
330
|
-
tunnel(host,
|
|
506
|
+
tunnel(host, port, clientSocket);
|
|
331
507
|
return;
|
|
332
508
|
}
|
|
333
|
-
const mandatoryAuth =
|
|
509
|
+
const mandatoryAuth = config.proxy.mandatory_auth ?? false;
|
|
510
|
+
const daemonMode = deps?.daemonMode ?? false;
|
|
334
511
|
let agentEmail;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
512
|
+
if (daemonMode) {
|
|
513
|
+
agentEmail = config.agents[0]?.email;
|
|
514
|
+
} else {
|
|
515
|
+
try {
|
|
516
|
+
const authHeader = req.headers["proxy-authorization"];
|
|
517
|
+
let identity = null;
|
|
518
|
+
for (const agentConf2 of config.agents) {
|
|
519
|
+
identity = await verifyAgentAuth(
|
|
520
|
+
authHeader ?? null,
|
|
521
|
+
agentConf2.idp_url,
|
|
522
|
+
mandatoryAuth && config.agents.length === 1
|
|
523
|
+
);
|
|
524
|
+
if (identity) break;
|
|
525
|
+
}
|
|
526
|
+
if (mandatoryAuth && !identity) {
|
|
527
|
+
throw new AuthError("JWT required");
|
|
528
|
+
}
|
|
529
|
+
agentEmail = identity?.email;
|
|
530
|
+
if (agentEmail) {
|
|
531
|
+
const known = config.agents.find((a) => a.email === agentEmail);
|
|
532
|
+
if (!known) {
|
|
533
|
+
clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
534
|
+
clientSocket.destroy();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
} else if (config.agents.length > 1) {
|
|
538
|
+
throw new AuthError("JWT required for multi-agent proxy");
|
|
539
|
+
}
|
|
540
|
+
} catch (err) {
|
|
541
|
+
if (err instanceof AuthError) {
|
|
542
|
+
clientSocket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
354
543
|
clientSocket.destroy();
|
|
355
544
|
return;
|
|
356
545
|
}
|
|
357
|
-
|
|
358
|
-
throw new AuthError("JWT required for multi-agent proxy");
|
|
359
|
-
}
|
|
360
|
-
} catch (err) {
|
|
361
|
-
if (err instanceof AuthError) {
|
|
362
|
-
clientSocket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
363
|
-
clientSocket.destroy();
|
|
364
|
-
return;
|
|
546
|
+
throw err;
|
|
365
547
|
}
|
|
366
|
-
throw err;
|
|
367
548
|
}
|
|
368
549
|
const egress = await checkEgress(host);
|
|
369
|
-
const auditAgent = agentEmail ??
|
|
550
|
+
const auditAgent = agentEmail ?? config.agents[0]?.email ?? "unknown";
|
|
370
551
|
if (egress.kind === "private") {
|
|
371
552
|
writeAudit({
|
|
372
553
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -401,7 +582,7 @@ DNS lookup failed for ${host} (${egress.reason}).\r
|
|
|
401
582
|
clientSocket.destroy();
|
|
402
583
|
return;
|
|
403
584
|
}
|
|
404
|
-
const agentConf = agentEmail ?
|
|
585
|
+
const agentConf = agentEmail ? config.agents.find((a) => a.email === agentEmail) : config.agents[0];
|
|
405
586
|
if (!agentConf) {
|
|
406
587
|
clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
407
588
|
clientSocket.destroy();
|
|
@@ -410,10 +591,10 @@ DNS lookup failed for ${host} (${egress.reason}).\r
|
|
|
410
591
|
const effectiveEmail = agentEmail ?? agentConf.email;
|
|
411
592
|
const rulesConfig = {
|
|
412
593
|
proxy: {
|
|
413
|
-
listen:
|
|
594
|
+
listen: config.proxy.listen,
|
|
414
595
|
idp_url: agentConf.idp_url,
|
|
415
596
|
agent_email: agentConf.email,
|
|
416
|
-
default_action:
|
|
597
|
+
default_action: config.proxy.default_action
|
|
417
598
|
},
|
|
418
599
|
allow: agentConf.allow ?? [],
|
|
419
600
|
deny: agentConf.deny ?? [],
|
|
@@ -454,7 +635,7 @@ Blocked:${note}\r
|
|
|
454
635
|
audience: "ape-proxy",
|
|
455
636
|
grantType: action.rule.grant_type,
|
|
456
637
|
permissions: action.rule.permissions,
|
|
457
|
-
reason: `CONNECT ${host}:${
|
|
638
|
+
reason: `CONNECT ${host}:${port}`,
|
|
458
639
|
duration: action.rule.duration
|
|
459
640
|
});
|
|
460
641
|
const decided = await grantsClient.waitForApproval(grant.id);
|
|
@@ -483,10 +664,40 @@ Grant request failed: ${msg}\r
|
|
|
483
664
|
} else {
|
|
484
665
|
writeAudit({ ...baseAudit, action: "allow", rule: "allow-list" });
|
|
485
666
|
}
|
|
486
|
-
|
|
667
|
+
if (deps?.secretsStore && deps?.leafCache) {
|
|
668
|
+
clientSocket.pause();
|
|
669
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
670
|
+
if (head.length > 0) {
|
|
671
|
+
clientSocket.unshift(head);
|
|
672
|
+
}
|
|
673
|
+
const store = deps.secretsStore;
|
|
674
|
+
handleMitmConnect({
|
|
675
|
+
clientSocket,
|
|
676
|
+
host,
|
|
677
|
+
port,
|
|
678
|
+
leafCache: deps.leafCache,
|
|
679
|
+
upstreamRejectUnauthorized: deps.upstreamRejectUnauthorized,
|
|
680
|
+
onRequest: (mreq) => {
|
|
681
|
+
const portSuffix = port === 443 ? "" : `:${port}`;
|
|
682
|
+
const targetUrl = new URL(`https://${mreq.host}${portSuffix}${mreq.path}`);
|
|
683
|
+
const match = store.findFor(targetUrl);
|
|
684
|
+
if (!match) return { type: "forward", mutatedHeaders: /* @__PURE__ */ new Map() };
|
|
685
|
+
const rendered = match.template.replace(/\$\{value\}/g, match.value);
|
|
686
|
+
console.error(
|
|
687
|
+
`[openape-proxy] injected secret '${match.name}' into ${match.header} for ${mreq.method} ${targetUrl.href}`
|
|
688
|
+
);
|
|
689
|
+
return {
|
|
690
|
+
type: "forward",
|
|
691
|
+
mutatedHeaders: /* @__PURE__ */ new Map([[match.header.toLowerCase(), rendered]])
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
tunnel(host, port, clientSocket);
|
|
487
698
|
}
|
|
488
|
-
function tunnel(host,
|
|
489
|
-
const targetSocket = connect(
|
|
699
|
+
function tunnel(host, port, clientSocket) {
|
|
700
|
+
const targetSocket = connect(port, host, () => {
|
|
490
701
|
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
491
702
|
targetSocket.pipe(clientSocket);
|
|
492
703
|
clientSocket.pipe(targetSocket);
|
|
@@ -512,30 +723,33 @@ async function computeRequestHash(method, targetUrl, body) {
|
|
|
512
723
|
}
|
|
513
724
|
return hash.digest("hex");
|
|
514
725
|
}
|
|
515
|
-
function buildGrantsClients(
|
|
726
|
+
function buildGrantsClients(config) {
|
|
516
727
|
const grantsClients = /* @__PURE__ */ new Map();
|
|
517
|
-
for (const agent of
|
|
728
|
+
for (const agent of config.agents) {
|
|
518
729
|
grantsClients.set(agent.email, new GrantsClient(agent.idp_url));
|
|
519
730
|
}
|
|
520
731
|
return grantsClients;
|
|
521
732
|
}
|
|
522
|
-
function createMultiAgentProxy(
|
|
523
|
-
for (const agent of
|
|
733
|
+
function createMultiAgentProxy(config, grantsClients = buildGrantsClients(config), deps = {}) {
|
|
734
|
+
for (const agent of config.agents) {
|
|
524
735
|
if (!grantsClients.has(agent.email)) {
|
|
525
736
|
grantsClients.set(agent.email, new GrantsClient(agent.idp_url));
|
|
526
737
|
}
|
|
527
738
|
}
|
|
528
|
-
const mandatoryAuth =
|
|
739
|
+
const mandatoryAuth = config.proxy.mandatory_auth ?? false;
|
|
740
|
+
const secretsStore = deps.secretsStore;
|
|
741
|
+
const daemonMode = deps.daemonMode ?? false;
|
|
742
|
+
const fetchImpl = deps.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
|
|
529
743
|
return {
|
|
530
|
-
port: Number.parseInt(
|
|
531
|
-
hostname:
|
|
744
|
+
port: Number.parseInt(config.proxy.listen.split(":")[1] || "9090"),
|
|
745
|
+
hostname: config.proxy.listen.split(":")[0] || "127.0.0.1",
|
|
532
746
|
async fetch(req) {
|
|
533
747
|
const url = new URL(req.url);
|
|
534
748
|
const startTime = Date.now();
|
|
535
749
|
if (url.pathname === "/healthz") {
|
|
536
750
|
return Response.json({
|
|
537
751
|
status: "ok",
|
|
538
|
-
agents:
|
|
752
|
+
agents: config.agents.map((a) => a.email)
|
|
539
753
|
});
|
|
540
754
|
}
|
|
541
755
|
const targetUrl = url.pathname.slice(1) + url.search;
|
|
@@ -563,33 +777,37 @@ function createMultiAgentProxy(config2, grantsClients = buildGrantsClients(confi
|
|
|
563
777
|
}
|
|
564
778
|
const bodyBuffer = req.body ? await req.arrayBuffer() : null;
|
|
565
779
|
let agentIdentity = null;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
780
|
+
if (daemonMode) {
|
|
781
|
+
agentIdentity = { email: config.agents[0].email, act: "agent" };
|
|
782
|
+
} else {
|
|
783
|
+
try {
|
|
784
|
+
for (const agentConf2 of config.agents) {
|
|
785
|
+
agentIdentity = await verifyAgentAuth(
|
|
786
|
+
req.headers.get("proxy-authorization"),
|
|
787
|
+
agentConf2.idp_url,
|
|
788
|
+
mandatoryAuth && config.agents.length === 1
|
|
789
|
+
);
|
|
790
|
+
if (agentIdentity) break;
|
|
791
|
+
}
|
|
792
|
+
if (mandatoryAuth && !agentIdentity) {
|
|
793
|
+
throw new AuthError("JWT required");
|
|
794
|
+
}
|
|
795
|
+
} catch (err) {
|
|
796
|
+
if (err instanceof AuthError) {
|
|
797
|
+
return new Response(`Unauthorized: ${err.message}`, { status: 401 });
|
|
798
|
+
}
|
|
799
|
+
throw err;
|
|
581
800
|
}
|
|
582
|
-
throw err;
|
|
583
801
|
}
|
|
584
802
|
const agentEmail = agentIdentity?.email;
|
|
585
803
|
let agentConf;
|
|
586
804
|
if (agentEmail) {
|
|
587
|
-
agentConf =
|
|
805
|
+
agentConf = config.agents.find((a) => a.email === agentEmail);
|
|
588
806
|
if (!agentConf) {
|
|
589
807
|
return new Response(`Forbidden: unknown agent ${agentEmail}`, { status: 403 });
|
|
590
808
|
}
|
|
591
|
-
} else if (
|
|
592
|
-
agentConf =
|
|
809
|
+
} else if (config.agents.length === 1) {
|
|
810
|
+
agentConf = config.agents[0];
|
|
593
811
|
} else {
|
|
594
812
|
return new Response("Unauthorized: JWT required for multi-agent proxy", { status: 401 });
|
|
595
813
|
}
|
|
@@ -597,10 +815,10 @@ function createMultiAgentProxy(config2, grantsClients = buildGrantsClients(confi
|
|
|
597
815
|
const grantsClient = grantsClients.get(agentConf.email);
|
|
598
816
|
const rulesConfig = {
|
|
599
817
|
proxy: {
|
|
600
|
-
listen:
|
|
818
|
+
listen: config.proxy.listen,
|
|
601
819
|
idp_url: agentConf.idp_url,
|
|
602
820
|
agent_email: agentConf.email,
|
|
603
|
-
default_action:
|
|
821
|
+
default_action: config.proxy.default_action
|
|
604
822
|
},
|
|
605
823
|
allow: agentConf.allow ?? [],
|
|
606
824
|
deny: agentConf.deny ?? [],
|
|
@@ -620,7 +838,7 @@ function createMultiAgentProxy(config2, grantsClients = buildGrantsClients(confi
|
|
|
620
838
|
}
|
|
621
839
|
if (action.type === "allow") {
|
|
622
840
|
writeAudit({ ...baseAudit, action: "allow", rule: "allow-list", grant_id: null });
|
|
623
|
-
return forwardRequest(req, targetUrl, bodyBuffer);
|
|
841
|
+
return forwardRequest(req, targetUrl, targetParsed, bodyBuffer, secretsStore, fetchImpl);
|
|
624
842
|
}
|
|
625
843
|
const rule = action.rule;
|
|
626
844
|
const permissions = rule.permissions ?? [`${method.toLowerCase()}:${domain}`];
|
|
@@ -639,13 +857,13 @@ function createMultiAgentProxy(config2, grantsClients = buildGrantsClients(confi
|
|
|
639
857
|
grant_id: existing.id,
|
|
640
858
|
request_hash: requestHash
|
|
641
859
|
});
|
|
642
|
-
return forwardRequest(req, targetUrl, bodyBuffer);
|
|
860
|
+
return forwardRequest(req, targetUrl, targetParsed, bodyBuffer, secretsStore, fetchImpl);
|
|
643
861
|
}
|
|
644
|
-
if (
|
|
862
|
+
if (config.proxy.default_action === "block") {
|
|
645
863
|
writeAudit({ ...baseAudit, action: "deny", rule: "no-grant (block mode)", grant_id: null });
|
|
646
864
|
return new Response("No grant \u2014 blocked", { status: 403 });
|
|
647
865
|
}
|
|
648
|
-
if (
|
|
866
|
+
if (config.proxy.default_action === "request-async") {
|
|
649
867
|
const grant = await grantsClient.requestGrant({
|
|
650
868
|
requester: effectiveEmail,
|
|
651
869
|
targetHost: domain,
|
|
@@ -694,7 +912,7 @@ function createMultiAgentProxy(config2, grantsClients = buildGrantsClients(confi
|
|
|
694
912
|
request_hash: requestHash,
|
|
695
913
|
waited_ms: waitedMs
|
|
696
914
|
});
|
|
697
|
-
return forwardRequest(req, targetUrl, bodyBuffer);
|
|
915
|
+
return forwardRequest(req, targetUrl, targetParsed, bodyBuffer, secretsStore, fetchImpl);
|
|
698
916
|
}
|
|
699
917
|
writeAudit({
|
|
700
918
|
...baseAudit,
|
|
@@ -717,9 +935,12 @@ function createMultiAgentProxy(config2, grantsClients = buildGrantsClients(confi
|
|
|
717
935
|
}
|
|
718
936
|
};
|
|
719
937
|
}
|
|
720
|
-
function createNodeHandler(
|
|
721
|
-
const grantsClients = buildGrantsClients(
|
|
722
|
-
const proxy = createMultiAgentProxy(
|
|
938
|
+
function createNodeHandler(config, deps = {}) {
|
|
939
|
+
const grantsClients = buildGrantsClients(config);
|
|
940
|
+
const proxy = createMultiAgentProxy(config, grantsClients, {
|
|
941
|
+
secretsStore: deps.secretsStore,
|
|
942
|
+
daemonMode: deps.daemonMode
|
|
943
|
+
});
|
|
723
944
|
return {
|
|
724
945
|
handleRequest(req, res) {
|
|
725
946
|
const reqUrl = req.url || "/";
|
|
@@ -767,24 +988,39 @@ function createNodeHandler(config2) {
|
|
|
767
988
|
});
|
|
768
989
|
},
|
|
769
990
|
handleConnect(req, socket, head) {
|
|
770
|
-
handleConnect(
|
|
991
|
+
handleConnect(config, grantsClients, req, socket, head, {
|
|
992
|
+
secretsStore: deps.secretsStore,
|
|
993
|
+
leafCache: deps.leafCache,
|
|
994
|
+
daemonMode: deps.daemonMode
|
|
995
|
+
});
|
|
771
996
|
}
|
|
772
997
|
};
|
|
773
998
|
}
|
|
774
|
-
async function forwardRequest(originalReq, targetUrl, cachedBody) {
|
|
999
|
+
async function forwardRequest(originalReq, targetUrl, targetParsed, cachedBody, secretsStore, fetchImpl) {
|
|
775
1000
|
const headers = new Headers(originalReq.headers);
|
|
776
1001
|
headers.delete("proxy-authorization");
|
|
777
1002
|
headers.delete("proxy-connection");
|
|
778
1003
|
headers.delete("host");
|
|
1004
|
+
if (secretsStore) {
|
|
1005
|
+
const match = secretsStore.findFor(targetParsed);
|
|
1006
|
+
if (match) {
|
|
1007
|
+
const rendered = match.template.replace(/\$\{value\}/g, match.value);
|
|
1008
|
+
headers.set(match.header, rendered);
|
|
1009
|
+
console.error(
|
|
1010
|
+
`[openape-proxy] injected secret '${match.name}' into ${match.header} for ${originalReq.method} ${targetUrl}`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
779
1014
|
const body = cachedBody && cachedBody.byteLength > 0 ? cachedBody : null;
|
|
780
1015
|
try {
|
|
781
|
-
const
|
|
1016
|
+
const upstreamReq = new Request(targetUrl, {
|
|
782
1017
|
method: originalReq.method,
|
|
783
1018
|
headers,
|
|
784
1019
|
body,
|
|
785
1020
|
duplex: "half",
|
|
786
1021
|
redirect: "manual"
|
|
787
1022
|
});
|
|
1023
|
+
const res = await fetchImpl(upstreamReq);
|
|
788
1024
|
const responseHeaders = new Headers(res.headers);
|
|
789
1025
|
responseHeaders.delete("transfer-encoding");
|
|
790
1026
|
responseHeaders.delete("connection");
|
|
@@ -799,56 +1035,220 @@ async function forwardRequest(originalReq, targetUrl, cachedBody) {
|
|
|
799
1035
|
}
|
|
800
1036
|
}
|
|
801
1037
|
|
|
1038
|
+
// src/secrets-store.ts
|
|
1039
|
+
import { parse as parseTOML2 } from "smol-toml";
|
|
1040
|
+
|
|
1041
|
+
// src/secrets-match.ts
|
|
1042
|
+
function compileGlob(glob) {
|
|
1043
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1044
|
+
return new RegExp(`^${escaped}$`);
|
|
1045
|
+
}
|
|
1046
|
+
function literalPrefixLen(glob) {
|
|
1047
|
+
const i = glob.indexOf("*");
|
|
1048
|
+
return i < 0 ? glob.length : i;
|
|
1049
|
+
}
|
|
1050
|
+
function targetString(url) {
|
|
1051
|
+
const port = url.port ? `:${url.port}` : "";
|
|
1052
|
+
const path = url.pathname === "/" ? "" : url.pathname;
|
|
1053
|
+
return `${url.hostname}${port}${path}`;
|
|
1054
|
+
}
|
|
1055
|
+
function matchSecret(target, entries) {
|
|
1056
|
+
const targetStr = targetString(target);
|
|
1057
|
+
let best = null;
|
|
1058
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1059
|
+
const entry = entries[i];
|
|
1060
|
+
const re = compileGlob(entry.target);
|
|
1061
|
+
if (!re.test(targetStr) && !re.test(`${targetStr}/`)) continue;
|
|
1062
|
+
const prefix = literalPrefixLen(entry.target);
|
|
1063
|
+
if (!best || prefix > best.prefix) {
|
|
1064
|
+
best = { entry, prefix, idx: i };
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return best?.entry ?? null;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/secrets-store.ts
|
|
1071
|
+
var MAX_BLOB_BYTES = 4 * 1024;
|
|
1072
|
+
var SUPPORTED_VERSION = "1";
|
|
1073
|
+
function parseSecretsBlob(toml) {
|
|
1074
|
+
if (Buffer.byteLength(toml, "utf-8") > MAX_BLOB_BYTES) {
|
|
1075
|
+
throw new Error(`Secrets blob too large (max ${MAX_BLOB_BYTES} bytes / 4 KiB)`);
|
|
1076
|
+
}
|
|
1077
|
+
const parsed = parseTOML2(toml);
|
|
1078
|
+
const version = parsed.version;
|
|
1079
|
+
if (version !== SUPPORTED_VERSION) {
|
|
1080
|
+
throw new Error(`Unsupported or missing version (expected "${SUPPORTED_VERSION}", got ${JSON.stringify(version)})`);
|
|
1081
|
+
}
|
|
1082
|
+
const secretsBlock = parsed.secrets;
|
|
1083
|
+
const entries = [];
|
|
1084
|
+
if (secretsBlock) {
|
|
1085
|
+
for (const [name, raw] of Object.entries(secretsBlock)) {
|
|
1086
|
+
const required = ["target", "header", "template", "value"];
|
|
1087
|
+
for (const field of required) {
|
|
1088
|
+
if (!raw[field] || typeof raw[field] !== "string") {
|
|
1089
|
+
throw new Error(`Secret '${name}' is missing required field '${field}'`);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
entries.push({
|
|
1093
|
+
name,
|
|
1094
|
+
target: raw.target,
|
|
1095
|
+
header: raw.header,
|
|
1096
|
+
template: raw.template,
|
|
1097
|
+
value: raw.value
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
const literalPrefix = (glob) => {
|
|
1102
|
+
const i = glob.indexOf("*");
|
|
1103
|
+
return i < 0 ? glob : glob.slice(0, i);
|
|
1104
|
+
};
|
|
1105
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1106
|
+
for (const e of entries) {
|
|
1107
|
+
const key = `${literalPrefix(e.target)}|${e.target}`;
|
|
1108
|
+
if (seen.has(key)) {
|
|
1109
|
+
throw new Error(`Duplicate target with identical specificity: '${e.target}' (entries '${seen.get(key)}' and '${e.name}')`);
|
|
1110
|
+
}
|
|
1111
|
+
seen.set(key, e.name);
|
|
1112
|
+
}
|
|
1113
|
+
return {
|
|
1114
|
+
entries,
|
|
1115
|
+
findFor: (target) => matchSecret(target, entries)
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
802
1119
|
// src/index.ts
|
|
1120
|
+
function loadIdentity() {
|
|
1121
|
+
const path = join(homedir(), ".config", "apes", "auth.json");
|
|
1122
|
+
if (!existsSync2(path)) {
|
|
1123
|
+
console.error(`[openape-proxy] missing ${path} \u2014 run \`apes login\` first`);
|
|
1124
|
+
process.exit(2);
|
|
1125
|
+
}
|
|
1126
|
+
const raw = JSON.parse(readFileSync3(path, "utf-8"));
|
|
1127
|
+
const bearer = raw.access_token ?? raw.bearer;
|
|
1128
|
+
if (!raw.email || !raw.idp || !bearer) {
|
|
1129
|
+
console.error(`[openape-proxy] malformed ${path} \u2014 re-run \`apes login\``);
|
|
1130
|
+
process.exit(2);
|
|
1131
|
+
}
|
|
1132
|
+
return { email: raw.email, idpUrl: raw.idp, bearer };
|
|
1133
|
+
}
|
|
803
1134
|
var { values } = parseArgs({
|
|
804
1135
|
options: {
|
|
805
1136
|
config: { type: "string", short: "c", default: "config.toml" },
|
|
806
1137
|
"dry-run": { type: "boolean", default: false },
|
|
807
|
-
"mandatory-auth": { type: "boolean", default: false }
|
|
1138
|
+
"mandatory-auth": { type: "boolean", default: false },
|
|
1139
|
+
global: { type: "boolean", default: false },
|
|
1140
|
+
port: { type: "string" }
|
|
808
1141
|
}
|
|
809
1142
|
});
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
console.log("[openape-proxy] DRY RUN mode \u2014 logging only, not blocking");
|
|
817
|
-
console.log("[openape-proxy] Config loaded:");
|
|
818
|
-
console.log(` Listen: ${config.proxy.listen}`);
|
|
819
|
-
console.log(` Default action: ${config.proxy.default_action}`);
|
|
820
|
-
console.log(` Mandatory auth: ${config.proxy.mandatory_auth ?? false}`);
|
|
821
|
-
console.log(` Agents: ${config.agents.length}`);
|
|
822
|
-
for (const agent of config.agents) {
|
|
823
|
-
const allowCount = agent.allow?.length ?? 0;
|
|
824
|
-
const denyCount = agent.deny?.length ?? 0;
|
|
825
|
-
const grantCount = agent.grant_required?.length ?? 0;
|
|
826
|
-
console.log(` ${agent.email} (${agent.idp_url}) \u2014 ${allowCount} allow, ${denyCount} deny, ${grantCount} grant`);
|
|
1143
|
+
if (values.global) {
|
|
1144
|
+
if (process.versions.bun) {
|
|
1145
|
+
console.error(
|
|
1146
|
+
"[openape-proxy] --global mode requires Node (Bun is not supported).\n Bun's node:tls compat layer does not handle TLSSocket-on-existing-socket\n for the CONNECT-MITM pipeline. Re-run with `node` directly, or use the\n built `openape-proxy` binary (its shebang invokes Node)."
|
|
1147
|
+
);
|
|
1148
|
+
process.exit(2);
|
|
827
1149
|
}
|
|
828
|
-
|
|
1150
|
+
const stdinBuf = [];
|
|
1151
|
+
let total = 0;
|
|
1152
|
+
let aborted = false;
|
|
1153
|
+
process.stdin.on("data", (chunk) => {
|
|
1154
|
+
if (aborted) return;
|
|
1155
|
+
total += chunk.length;
|
|
1156
|
+
if (total > 8 * 1024) {
|
|
1157
|
+
console.error("[openape-proxy] stdin > 8 KiB, refusing");
|
|
1158
|
+
aborted = true;
|
|
1159
|
+
process.exit(2);
|
|
1160
|
+
}
|
|
1161
|
+
stdinBuf.push(chunk);
|
|
1162
|
+
});
|
|
1163
|
+
process.stdin.on("end", () => {
|
|
1164
|
+
if (aborted) return;
|
|
1165
|
+
const blob = Buffer.concat(stdinBuf).toString("utf-8");
|
|
1166
|
+
if (!blob.trim()) {
|
|
1167
|
+
console.error("[openape-proxy] --global requires secrets TOML on stdin");
|
|
1168
|
+
process.exit(2);
|
|
1169
|
+
}
|
|
1170
|
+
let store;
|
|
1171
|
+
try {
|
|
1172
|
+
store = parseSecretsBlob(blob);
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
console.error(`[openape-proxy] secrets parse error: ${err.message}`);
|
|
1175
|
+
process.exit(2);
|
|
1176
|
+
}
|
|
1177
|
+
const identity = loadIdentity();
|
|
1178
|
+
const port = Number.parseInt(values.port ?? "18789");
|
|
1179
|
+
const ca = loadOrCreateCa({
|
|
1180
|
+
certPath: join(homedir(), ".openape", "proxy", "ca.crt"),
|
|
1181
|
+
keyPath: join(homedir(), ".openape", "proxy", "ca.key"),
|
|
1182
|
+
subjectCN: `OpenApe Proxy CA (${identity.email})`
|
|
1183
|
+
});
|
|
1184
|
+
if (ca.created) {
|
|
1185
|
+
console.log(`[openape-proxy] generated new CA at ~/.openape/proxy/ca.crt`);
|
|
1186
|
+
}
|
|
1187
|
+
const leafCache = createLeafCertCache(ca, { capacity: 256 });
|
|
1188
|
+
const config = {
|
|
1189
|
+
proxy: { listen: `127.0.0.1:${port}`, default_action: "allow" },
|
|
1190
|
+
agents: [{ email: identity.email, idp_url: identity.idpUrl }]
|
|
1191
|
+
};
|
|
1192
|
+
const handler = createNodeHandler(config, { secretsStore: store, leafCache, daemonMode: true });
|
|
1193
|
+
const server = createServer(handler.handleRequest);
|
|
1194
|
+
server.on("connect", handler.handleConnect);
|
|
1195
|
+
server.listen(port, "127.0.0.1", () => {
|
|
1196
|
+
const addr = server.address();
|
|
1197
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
1198
|
+
console.log(`[openape-proxy] identity: ${identity.email} (${identity.idpUrl})`);
|
|
1199
|
+
const names = store.entries.map((e) => e.name).join(", ");
|
|
1200
|
+
console.log(`[openape-proxy] secrets: ${names}`);
|
|
1201
|
+
console.log(`[openape-proxy] export OPENAPE_PROXY=127.0.0.1:${actualPort}`);
|
|
1202
|
+
console.log(`[openape-proxy] listening on 127.0.0.1:${actualPort}`);
|
|
1203
|
+
});
|
|
1204
|
+
process.on("SIGINT", () => server.close(() => process.exit(0)));
|
|
1205
|
+
process.on("SIGTERM", () => server.close(() => process.exit(0)));
|
|
1206
|
+
});
|
|
1207
|
+
process.stdin.resume();
|
|
1208
|
+
} else {
|
|
1209
|
+
const configPath = values.config;
|
|
1210
|
+
console.log(`[openape-proxy] Loading config from ${configPath}`);
|
|
1211
|
+
const config = loadMultiAgentConfig(configPath, {
|
|
1212
|
+
mandatoryAuth: values["mandatory-auth"] || void 0
|
|
1213
|
+
});
|
|
1214
|
+
if (values["dry-run"]) {
|
|
1215
|
+
console.log("[openape-proxy] DRY RUN mode \u2014 logging only, not blocking");
|
|
1216
|
+
console.log("[openape-proxy] Config loaded:");
|
|
1217
|
+
console.log(` Listen: ${config.proxy.listen}`);
|
|
1218
|
+
console.log(` Default action: ${config.proxy.default_action}`);
|
|
1219
|
+
console.log(` Mandatory auth: ${config.proxy.mandatory_auth ?? false}`);
|
|
1220
|
+
console.log(` Agents: ${config.agents.length}`);
|
|
1221
|
+
for (const agent of config.agents) {
|
|
1222
|
+
const allowCount = agent.allow?.length ?? 0;
|
|
1223
|
+
const denyCount = agent.deny?.length ?? 0;
|
|
1224
|
+
const grantCount = agent.grant_required?.length ?? 0;
|
|
1225
|
+
console.log(` ${agent.email} (${agent.idp_url}) \u2014 ${allowCount} allow, ${denyCount} deny, ${grantCount} grant`);
|
|
1226
|
+
}
|
|
1227
|
+
process.exit(0);
|
|
1228
|
+
}
|
|
1229
|
+
const handler = createNodeHandler(config);
|
|
1230
|
+
const port = Number.parseInt(config.proxy.listen.split(":")[1] || "9090");
|
|
1231
|
+
const hostname = config.proxy.listen.split(":")[0] || "127.0.0.1";
|
|
1232
|
+
const server = createServer(handler.handleRequest);
|
|
1233
|
+
server.on("connect", handler.handleConnect);
|
|
1234
|
+
server.listen(port, hostname, () => {
|
|
1235
|
+
const addr = server.address();
|
|
1236
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
1237
|
+
console.log(`[openape-proxy] Listening on http://${hostname}:${actualPort}`);
|
|
1238
|
+
console.log(`[openape-proxy] CONNECT tunneling enabled`);
|
|
1239
|
+
console.log(`[openape-proxy] Mandatory auth: ${config.proxy.mandatory_auth ?? false}`);
|
|
1240
|
+
console.log(`[openape-proxy] Agents: ${config.agents.map((a) => a.email).join(", ")}`);
|
|
1241
|
+
console.log(`[openape-proxy] Default action: ${config.proxy.default_action}`);
|
|
1242
|
+
});
|
|
1243
|
+
process.on("SIGINT", () => {
|
|
1244
|
+
console.log("\n[openape-proxy] Shutting down...");
|
|
1245
|
+
server.close();
|
|
1246
|
+
process.exit(0);
|
|
1247
|
+
});
|
|
1248
|
+
process.on("SIGTERM", () => {
|
|
1249
|
+
console.log("[openape-proxy] Shutting down...");
|
|
1250
|
+
server.close();
|
|
1251
|
+
process.exit(0);
|
|
1252
|
+
});
|
|
829
1253
|
}
|
|
830
|
-
var handler = createNodeHandler(config);
|
|
831
|
-
var port = Number.parseInt(config.proxy.listen.split(":")[1] || "9090");
|
|
832
|
-
var hostname = config.proxy.listen.split(":")[0] || "127.0.0.1";
|
|
833
|
-
var server = createServer(handler.handleRequest);
|
|
834
|
-
server.on("connect", handler.handleConnect);
|
|
835
|
-
server.listen(port, hostname, () => {
|
|
836
|
-
const addr = server.address();
|
|
837
|
-
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
838
|
-
console.log(`[openape-proxy] Listening on http://${hostname}:${actualPort}`);
|
|
839
|
-
console.log(`[openape-proxy] CONNECT tunneling enabled`);
|
|
840
|
-
console.log(`[openape-proxy] Mandatory auth: ${config.proxy.mandatory_auth ?? false}`);
|
|
841
|
-
console.log(`[openape-proxy] Agents: ${config.agents.map((a) => a.email).join(", ")}`);
|
|
842
|
-
console.log(`[openape-proxy] Default action: ${config.proxy.default_action}`);
|
|
843
|
-
});
|
|
844
|
-
process.on("SIGINT", () => {
|
|
845
|
-
console.log("\n[openape-proxy] Shutting down...");
|
|
846
|
-
server.close();
|
|
847
|
-
process.exit(0);
|
|
848
|
-
});
|
|
849
|
-
process.on("SIGTERM", () => {
|
|
850
|
-
console.log("[openape-proxy] Shutting down...");
|
|
851
|
-
server.close();
|
|
852
|
-
process.exit(0);
|
|
853
|
-
});
|
|
854
1254
|
//# sourceMappingURL=index.js.map
|