smartcontext-proxy 0.1.0 → 0.2.1
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/PLAN-v2.md +390 -0
- package/dist/src/context/ab-test.d.ts +32 -0
- package/dist/src/context/ab-test.js +133 -0
- package/dist/src/index.js +99 -78
- package/dist/src/proxy/classifier.d.ts +14 -0
- package/dist/src/proxy/classifier.js +63 -0
- package/dist/src/proxy/connect-proxy.d.ts +37 -0
- package/dist/src/proxy/connect-proxy.js +234 -0
- package/dist/src/proxy/server.js +10 -1
- package/dist/src/proxy/tls-interceptor.d.ts +23 -0
- package/dist/src/proxy/tls-interceptor.js +211 -0
- package/dist/src/proxy/transparent-listener.d.ts +31 -0
- package/dist/src/proxy/transparent-listener.js +285 -0
- package/dist/src/proxy/tunnel.d.ts +7 -0
- package/dist/src/proxy/tunnel.js +33 -0
- package/dist/src/system/dns-redirect.d.ts +28 -0
- package/dist/src/system/dns-redirect.js +141 -0
- package/dist/src/system/installer.d.ts +25 -0
- package/dist/src/system/installer.js +180 -0
- package/dist/src/system/linux.d.ts +11 -0
- package/dist/src/system/linux.js +60 -0
- package/dist/src/system/macos.d.ts +24 -0
- package/dist/src/system/macos.js +98 -0
- package/dist/src/system/pf-redirect.d.ts +25 -0
- package/dist/src/system/pf-redirect.js +177 -0
- package/dist/src/system/watchdog.d.ts +7 -0
- package/dist/src/system/watchdog.js +115 -0
- package/dist/src/test/connect-proxy.test.d.ts +1 -0
- package/dist/src/test/connect-proxy.test.js +147 -0
- package/dist/src/test/dashboard.test.js +1 -0
- package/dist/src/tls/ca-manager.d.ts +9 -0
- package/dist/src/tls/ca-manager.js +117 -0
- package/dist/src/tls/trust-store.d.ts +11 -0
- package/dist/src/tls/trust-store.js +121 -0
- package/dist/src/tray/bridge.d.ts +8 -0
- package/dist/src/tray/bridge.js +66 -0
- package/dist/src/ui/dashboard.d.ts +10 -1
- package/dist/src/ui/dashboard.js +119 -34
- package/dist/src/ui/ws-feed.d.ts +8 -0
- package/dist/src/ui/ws-feed.js +30 -0
- package/native/macos/SmartContextTray/Package.swift +13 -0
- package/native/macos/SmartContextTray/Sources/main.swift +206 -0
- package/package.json +6 -2
- package/src/context/ab-test.ts +172 -0
- package/src/index.ts +104 -74
- package/src/proxy/classifier.ts +71 -0
- package/src/proxy/connect-proxy.ts +251 -0
- package/src/proxy/server.ts +11 -2
- package/src/proxy/tls-interceptor.ts +261 -0
- package/src/proxy/transparent-listener.ts +328 -0
- package/src/proxy/tunnel.ts +32 -0
- package/src/system/dns-redirect.ts +144 -0
- package/src/system/installer.ts +148 -0
- package/src/system/linux.ts +57 -0
- package/src/system/macos.ts +89 -0
- package/src/system/pf-redirect.ts +175 -0
- package/src/system/watchdog.ts +76 -0
- package/src/test/connect-proxy.test.ts +170 -0
- package/src/test/dashboard.test.ts +1 -0
- package/src/tls/ca-manager.ts +140 -0
- package/src/tls/trust-store.ts +123 -0
- package/src/tray/bridge.ts +61 -0
- package/src/ui/dashboard.ts +129 -35
- package/src/ui/ws-feed.ts +32 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.interceptTLS = interceptTLS;
|
|
7
|
+
const node_tls_1 = __importDefault(require("node:tls"));
|
|
8
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
9
|
+
const node_https_1 = __importDefault(require("node:https"));
|
|
10
|
+
const ca_manager_js_1 = require("../tls/ca-manager.js");
|
|
11
|
+
const chunker_js_1 = require("../context/chunker.js");
|
|
12
|
+
const canonical_js_1 = require("../context/canonical.js");
|
|
13
|
+
/**
|
|
14
|
+
* Intercept TLS connection to an LLM provider.
|
|
15
|
+
* Terminates TLS with a generated cert, parses the HTTP request inside,
|
|
16
|
+
* optionally optimizes context, then forwards to the real provider.
|
|
17
|
+
*/
|
|
18
|
+
function interceptTLS(clientSocket, hostname, port, match, options) {
|
|
19
|
+
const { cert, key } = (0, ca_manager_js_1.getCertForHost)(hostname);
|
|
20
|
+
const tlsSocket = new node_tls_1.default.TLSSocket(clientSocket, {
|
|
21
|
+
isServer: true,
|
|
22
|
+
cert,
|
|
23
|
+
key,
|
|
24
|
+
});
|
|
25
|
+
// Tell client the tunnel is established
|
|
26
|
+
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
27
|
+
// Read HTTP request from decrypted TLS stream
|
|
28
|
+
let requestData = Buffer.alloc(0);
|
|
29
|
+
let headersParsed = false;
|
|
30
|
+
let contentLength = 0;
|
|
31
|
+
let headerEnd = -1;
|
|
32
|
+
tlsSocket.on('data', (chunk) => {
|
|
33
|
+
requestData = Buffer.concat([requestData, chunk]);
|
|
34
|
+
if (!headersParsed) {
|
|
35
|
+
headerEnd = requestData.indexOf('\r\n\r\n');
|
|
36
|
+
if (headerEnd === -1)
|
|
37
|
+
return; // Wait for more data
|
|
38
|
+
headersParsed = true;
|
|
39
|
+
const headersStr = requestData.subarray(0, headerEnd).toString();
|
|
40
|
+
const clMatch = headersStr.match(/content-length:\s*(\d+)/i);
|
|
41
|
+
contentLength = clMatch ? parseInt(clMatch[1], 10) : 0;
|
|
42
|
+
}
|
|
43
|
+
const bodyStart = headerEnd + 4;
|
|
44
|
+
const bodyReceived = requestData.length - bodyStart;
|
|
45
|
+
if (bodyReceived >= contentLength) {
|
|
46
|
+
tlsSocket.pause();
|
|
47
|
+
handleInterceptedRequest(requestData, hostname, port, match, options, tlsSocket).catch((err) => {
|
|
48
|
+
options.log('error', `Intercept error: ${err}`);
|
|
49
|
+
try {
|
|
50
|
+
tlsSocket.write('HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n');
|
|
51
|
+
tlsSocket.end();
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
tlsSocket.on('error', (err) => {
|
|
58
|
+
options.log('error', `TLS socket error for ${hostname}: ${err.message}`);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Handle an intercepted HTTP request: parse, optimize, forward, stream back.
|
|
63
|
+
*/
|
|
64
|
+
async function handleInterceptedRequest(rawRequest, hostname, port, match, options, clientTLS) {
|
|
65
|
+
const startTime = Date.now();
|
|
66
|
+
options.requestCounter.value++;
|
|
67
|
+
const reqId = options.requestCounter.value;
|
|
68
|
+
// Parse raw HTTP request
|
|
69
|
+
const headerEnd = rawRequest.indexOf('\r\n\r\n');
|
|
70
|
+
const headersStr = rawRequest.subarray(0, headerEnd).toString();
|
|
71
|
+
const body = rawRequest.subarray(headerEnd + 4);
|
|
72
|
+
const [requestLine, ...headerLines] = headersStr.split('\r\n');
|
|
73
|
+
const [method, path] = requestLine.split(' ');
|
|
74
|
+
const headers = {};
|
|
75
|
+
for (const line of headerLines) {
|
|
76
|
+
const colonIdx = line.indexOf(':');
|
|
77
|
+
if (colonIdx > 0) {
|
|
78
|
+
headers[line.substring(0, colonIdx).toLowerCase().trim()] = line.substring(colonIdx + 1).trim();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Find adapter for this provider
|
|
82
|
+
const adapter = options.adapters.get(match.provider);
|
|
83
|
+
let forwardBody;
|
|
84
|
+
let originalTokens = 0;
|
|
85
|
+
let optimizedTokens = 0;
|
|
86
|
+
let savingsPercent = 0;
|
|
87
|
+
let chunksRetrieved = 0;
|
|
88
|
+
let topScore = 0;
|
|
89
|
+
let passThrough = true;
|
|
90
|
+
let reason;
|
|
91
|
+
let model = 'unknown';
|
|
92
|
+
if (adapter && body.length > 0) {
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(body.toString());
|
|
95
|
+
model = parsed.model || 'unknown';
|
|
96
|
+
const canonical = adapter.parseRequest(parsed, headers);
|
|
97
|
+
originalTokens = (0, chunker_js_1.estimateTokens)(canonical.systemPrompt || '') +
|
|
98
|
+
canonical.messages.reduce((sum, m) => sum + (0, chunker_js_1.estimateTokens)((0, canonical_js_1.getTextContent)(m)), 0);
|
|
99
|
+
// Optimize if available and not paused
|
|
100
|
+
if (options.optimizer && !options.paused) {
|
|
101
|
+
try {
|
|
102
|
+
const result = await options.optimizer.optimize(canonical);
|
|
103
|
+
passThrough = result.passThrough;
|
|
104
|
+
reason = result.reason;
|
|
105
|
+
if (!result.passThrough) {
|
|
106
|
+
canonical.messages = result.optimizedMessages;
|
|
107
|
+
if (result.systemPrompt !== undefined)
|
|
108
|
+
canonical.systemPrompt = result.systemPrompt;
|
|
109
|
+
optimizedTokens = result.packed.optimizedTokens;
|
|
110
|
+
savingsPercent = result.packed.savingsPercent;
|
|
111
|
+
}
|
|
112
|
+
if (result.retrieval) {
|
|
113
|
+
chunksRetrieved = result.retrieval.chunks.length;
|
|
114
|
+
topScore = result.retrieval.topScore;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
passThrough = true;
|
|
119
|
+
reason = `optimization error: ${err}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!passThrough) {
|
|
123
|
+
forwardBody = Buffer.from(JSON.stringify(adapter.serializeRequest(canonical)));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
forwardBody = body;
|
|
127
|
+
optimizedTokens = originalTokens;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
forwardBody = body;
|
|
132
|
+
optimizedTokens = originalTokens;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
forwardBody = body;
|
|
137
|
+
}
|
|
138
|
+
const latencyOverhead = Date.now() - startTime;
|
|
139
|
+
const savingsStr = passThrough ? 'pass' : `-${savingsPercent}%`;
|
|
140
|
+
options.log('info', `#${reqId} ${match.provider}/${model} ${originalTokens}→${optimizedTokens} ${savingsStr} ${latencyOverhead}ms [intercepted]`);
|
|
141
|
+
// Forward to real provider
|
|
142
|
+
const useHTTPS = port === 443 || hostname.includes('.');
|
|
143
|
+
const transport = useHTTPS ? node_https_1.default : node_http_1.default;
|
|
144
|
+
const forwardUrl = `${useHTTPS ? 'https' : 'http'}://${hostname}:${port}${path}`;
|
|
145
|
+
const forwardHeaders = { ...headers };
|
|
146
|
+
forwardHeaders['host'] = hostname;
|
|
147
|
+
forwardHeaders['content-length'] = String(forwardBody.length);
|
|
148
|
+
const providerRes = await new Promise((resolve, reject) => {
|
|
149
|
+
const proxyReq = transport.request(forwardUrl, {
|
|
150
|
+
method,
|
|
151
|
+
headers: forwardHeaders,
|
|
152
|
+
}, resolve);
|
|
153
|
+
proxyReq.on('error', reject);
|
|
154
|
+
proxyReq.write(forwardBody);
|
|
155
|
+
proxyReq.end();
|
|
156
|
+
});
|
|
157
|
+
// Build response headers
|
|
158
|
+
const resHeaders = [
|
|
159
|
+
`HTTP/1.1 ${providerRes.statusCode} ${providerRes.statusMessage}`,
|
|
160
|
+
];
|
|
161
|
+
for (const [key, val] of Object.entries(providerRes.headers)) {
|
|
162
|
+
if (val) {
|
|
163
|
+
const values = Array.isArray(val) ? val : [val];
|
|
164
|
+
for (const v of values) {
|
|
165
|
+
resHeaders.push(`${key}: ${v}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
resHeaders.push('', '');
|
|
170
|
+
clientTLS.write(resHeaders.join('\r\n'));
|
|
171
|
+
// Stream response body
|
|
172
|
+
await new Promise((resolve) => {
|
|
173
|
+
providerRes.on('data', (chunk) => {
|
|
174
|
+
clientTLS.write(chunk);
|
|
175
|
+
});
|
|
176
|
+
providerRes.on('end', () => {
|
|
177
|
+
clientTLS.end();
|
|
178
|
+
resolve();
|
|
179
|
+
});
|
|
180
|
+
providerRes.on('error', () => {
|
|
181
|
+
clientTLS.end();
|
|
182
|
+
resolve();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
// Record metrics
|
|
186
|
+
options.metrics.record({
|
|
187
|
+
id: reqId,
|
|
188
|
+
timestamp: Date.now(),
|
|
189
|
+
provider: match.provider,
|
|
190
|
+
model,
|
|
191
|
+
streaming: headers['accept']?.includes('text/event-stream') || false,
|
|
192
|
+
originalTokens,
|
|
193
|
+
optimizedTokens,
|
|
194
|
+
savingsPercent,
|
|
195
|
+
latencyOverheadMs: latencyOverhead,
|
|
196
|
+
chunksRetrieved,
|
|
197
|
+
topScore,
|
|
198
|
+
passThrough,
|
|
199
|
+
reason,
|
|
200
|
+
});
|
|
201
|
+
// Async post-indexing
|
|
202
|
+
if (options.optimizer && !passThrough && adapter) {
|
|
203
|
+
try {
|
|
204
|
+
const parsed = JSON.parse(body.toString());
|
|
205
|
+
const canonical = adapter.parseRequest(parsed, headers);
|
|
206
|
+
options.optimizer.indexExchange(canonical.messages, `intercepted-${reqId}`).catch(() => { });
|
|
207
|
+
}
|
|
208
|
+
catch { }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=tls-interceptor.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ProviderAdapter } from '../providers/types.js';
|
|
2
|
+
import type { SmartContextConfig } from '../config/schema.js';
|
|
3
|
+
import { ContextOptimizer } from '../context/optimizer.js';
|
|
4
|
+
import { MetricsCollector } from '../metrics/collector.js';
|
|
5
|
+
export interface TransparentOptions {
|
|
6
|
+
config: SmartContextConfig;
|
|
7
|
+
optimizer: ContextOptimizer | null;
|
|
8
|
+
metrics: MetricsCollector;
|
|
9
|
+
adapters: Map<string, ProviderAdapter>;
|
|
10
|
+
paused: boolean;
|
|
11
|
+
requestCounter: {
|
|
12
|
+
value: number;
|
|
13
|
+
};
|
|
14
|
+
log: (level: string, message: string) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Transparent TLS listener on port 443.
|
|
18
|
+
* When DNS redirects LLM traffic to 127.0.0.1, clients connect directly
|
|
19
|
+
* via TLS (no CONNECT). This server terminates TLS using SNI to pick
|
|
20
|
+
* the right cert, then handles the request.
|
|
21
|
+
*/
|
|
22
|
+
export declare class TransparentListener {
|
|
23
|
+
private server;
|
|
24
|
+
private options;
|
|
25
|
+
constructor(options: TransparentOptions);
|
|
26
|
+
start(port?: number, host?: string): Promise<void>;
|
|
27
|
+
stop(): Promise<void>;
|
|
28
|
+
private handleConnection;
|
|
29
|
+
private handleTLSConnection;
|
|
30
|
+
private handleRequest;
|
|
31
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TransparentListener = void 0;
|
|
7
|
+
const node_tls_1 = __importDefault(require("node:tls"));
|
|
8
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
9
|
+
const node_https_1 = __importDefault(require("node:https"));
|
|
10
|
+
const ca_manager_js_1 = require("../tls/ca-manager.js");
|
|
11
|
+
const classifier_js_1 = require("./classifier.js");
|
|
12
|
+
const dns_redirect_js_1 = require("../system/dns-redirect.js");
|
|
13
|
+
const chunker_js_1 = require("../context/chunker.js");
|
|
14
|
+
const canonical_js_1 = require("../context/canonical.js");
|
|
15
|
+
/**
|
|
16
|
+
* Transparent TLS listener on port 443.
|
|
17
|
+
* When DNS redirects LLM traffic to 127.0.0.1, clients connect directly
|
|
18
|
+
* via TLS (no CONNECT). This server terminates TLS using SNI to pick
|
|
19
|
+
* the right cert, then handles the request.
|
|
20
|
+
*/
|
|
21
|
+
class TransparentListener {
|
|
22
|
+
server;
|
|
23
|
+
options;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.options = options;
|
|
26
|
+
// Raw TCP server — we need SNI from ClientHello before creating TLS context
|
|
27
|
+
this.server = node_net_1.default.createServer((socket) => this.handleConnection(socket));
|
|
28
|
+
}
|
|
29
|
+
async start(port = 443, host = '127.0.0.1') {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
this.server.on('error', (err) => {
|
|
32
|
+
if (err.code === 'EACCES') {
|
|
33
|
+
this.options.log('error', `Port ${port} requires root. Transparent listener disabled.`);
|
|
34
|
+
resolve(); // Non-fatal
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
reject(err);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
this.server.listen(port, host, () => {
|
|
41
|
+
this.options.log('info', `Transparent TLS listener on ${host}:${port}`);
|
|
42
|
+
resolve();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async stop() {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
this.server.close(() => resolve());
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
handleConnection(socket) {
|
|
52
|
+
// Peek at first bytes to extract SNI from TLS ClientHello
|
|
53
|
+
socket.once('data', (data) => {
|
|
54
|
+
const hostname = extractSNI(data);
|
|
55
|
+
if (!hostname) {
|
|
56
|
+
socket.destroy();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const match = (0, classifier_js_1.classifyHost)(hostname, 443);
|
|
60
|
+
if (!match) {
|
|
61
|
+
// Not an LLM host — shouldn't happen with DNS redirect, but just in case
|
|
62
|
+
socket.destroy();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.options.log('info', `TRANSPARENT ${hostname} (${match.provider})`);
|
|
66
|
+
const { cert, key } = (0, ca_manager_js_1.getCertForHost)(hostname);
|
|
67
|
+
const tlsSocket = new node_tls_1.default.TLSSocket(socket, {
|
|
68
|
+
isServer: true,
|
|
69
|
+
cert,
|
|
70
|
+
key,
|
|
71
|
+
});
|
|
72
|
+
// Unshift the peeked data back
|
|
73
|
+
tlsSocket.unshift(data);
|
|
74
|
+
// Actually we already consumed data from socket, TLSSocket wraps socket
|
|
75
|
+
// Need to handle this differently — push data through
|
|
76
|
+
this.handleTLSConnection(tlsSocket, hostname, match);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
handleTLSConnection(tlsSocket, hostname, match) {
|
|
80
|
+
let requestData = Buffer.alloc(0);
|
|
81
|
+
let headersParsed = false;
|
|
82
|
+
let contentLength = 0;
|
|
83
|
+
let headerEnd = -1;
|
|
84
|
+
tlsSocket.on('data', (chunk) => {
|
|
85
|
+
requestData = Buffer.concat([requestData, chunk]);
|
|
86
|
+
if (!headersParsed) {
|
|
87
|
+
headerEnd = requestData.indexOf('\r\n\r\n');
|
|
88
|
+
if (headerEnd === -1)
|
|
89
|
+
return;
|
|
90
|
+
headersParsed = true;
|
|
91
|
+
const headersStr = requestData.subarray(0, headerEnd).toString();
|
|
92
|
+
const clMatch = headersStr.match(/content-length:\s*(\d+)/i);
|
|
93
|
+
contentLength = clMatch ? parseInt(clMatch[1], 10) : 0;
|
|
94
|
+
}
|
|
95
|
+
const bodyStart = headerEnd + 4;
|
|
96
|
+
const bodyReceived = requestData.length - bodyStart;
|
|
97
|
+
if (bodyReceived >= contentLength) {
|
|
98
|
+
tlsSocket.pause();
|
|
99
|
+
this.handleRequest(requestData, hostname, match, tlsSocket).catch((err) => {
|
|
100
|
+
this.options.log('error', `Transparent handler error: ${err}`);
|
|
101
|
+
try {
|
|
102
|
+
tlsSocket.end();
|
|
103
|
+
}
|
|
104
|
+
catch { }
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
tlsSocket.on('error', (err) => {
|
|
109
|
+
this.options.log('error', `TLS error for ${hostname}: ${err.message}`);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async handleRequest(rawRequest, hostname, match, clientTLS) {
|
|
113
|
+
this.options.requestCounter.value++;
|
|
114
|
+
const reqId = this.options.requestCounter.value;
|
|
115
|
+
const startTime = Date.now();
|
|
116
|
+
// Parse HTTP request
|
|
117
|
+
const headerEnd = rawRequest.indexOf('\r\n\r\n');
|
|
118
|
+
const headersStr = rawRequest.subarray(0, headerEnd).toString();
|
|
119
|
+
const body = rawRequest.subarray(headerEnd + 4);
|
|
120
|
+
const [requestLine, ...headerLines] = headersStr.split('\r\n');
|
|
121
|
+
const [method, reqPath] = requestLine.split(' ');
|
|
122
|
+
const headers = {};
|
|
123
|
+
for (const line of headerLines) {
|
|
124
|
+
const colonIdx = line.indexOf(':');
|
|
125
|
+
if (colonIdx > 0) {
|
|
126
|
+
headers[line.substring(0, colonIdx).toLowerCase().trim()] = line.substring(colonIdx + 1).trim();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Get real IP for this hostname (cached before DNS override)
|
|
130
|
+
const realIP = (0, dns_redirect_js_1.getRealIP)(hostname);
|
|
131
|
+
if (!realIP) {
|
|
132
|
+
this.options.log('error', `No real IP cached for ${hostname}`);
|
|
133
|
+
clientTLS.write('HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n');
|
|
134
|
+
clientTLS.end();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Optimization logic
|
|
138
|
+
const adapter = this.options.adapters.get(match.provider);
|
|
139
|
+
let forwardBody = body;
|
|
140
|
+
let model = 'unknown';
|
|
141
|
+
let originalTokens = 0;
|
|
142
|
+
let optimizedTokens = 0;
|
|
143
|
+
let savingsPercent = 0;
|
|
144
|
+
let passThrough = true;
|
|
145
|
+
if (adapter && body.length > 0) {
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(body.toString());
|
|
148
|
+
model = parsed.model || 'unknown';
|
|
149
|
+
const canonical = adapter.parseRequest(parsed, headers);
|
|
150
|
+
originalTokens = (0, chunker_js_1.estimateTokens)(canonical.systemPrompt || '') +
|
|
151
|
+
canonical.messages.reduce((sum, m) => sum + (0, chunker_js_1.estimateTokens)((0, canonical_js_1.getTextContent)(m)), 0);
|
|
152
|
+
if (this.options.optimizer && !this.options.paused) {
|
|
153
|
+
try {
|
|
154
|
+
const result = await this.options.optimizer.optimize(canonical);
|
|
155
|
+
passThrough = result.passThrough;
|
|
156
|
+
if (!result.passThrough) {
|
|
157
|
+
canonical.messages = result.optimizedMessages;
|
|
158
|
+
if (result.systemPrompt !== undefined)
|
|
159
|
+
canonical.systemPrompt = result.systemPrompt;
|
|
160
|
+
optimizedTokens = result.packed.optimizedTokens;
|
|
161
|
+
savingsPercent = result.packed.savingsPercent;
|
|
162
|
+
forwardBody = Buffer.from(JSON.stringify(adapter.serializeRequest(canonical)));
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
optimizedTokens = originalTokens;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
optimizedTokens = originalTokens;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
optimizedTokens = originalTokens;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
optimizedTokens = originalTokens;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const latency = Date.now() - startTime;
|
|
181
|
+
const savingsStr = passThrough ? 'pass' : `-${savingsPercent}%`;
|
|
182
|
+
this.options.log('info', `#${reqId} ${match.provider}/${model} ${originalTokens}→${optimizedTokens} ${savingsStr} ${latency}ms [transparent]`);
|
|
183
|
+
// Forward to real provider IP
|
|
184
|
+
const forwardHeaders = { ...headers };
|
|
185
|
+
forwardHeaders['host'] = hostname;
|
|
186
|
+
forwardHeaders['content-length'] = String(forwardBody.length);
|
|
187
|
+
const providerRes = await new Promise((resolve, reject) => {
|
|
188
|
+
const req = node_https_1.default.request({
|
|
189
|
+
hostname: realIP,
|
|
190
|
+
port: 443,
|
|
191
|
+
path: reqPath,
|
|
192
|
+
method,
|
|
193
|
+
headers: forwardHeaders,
|
|
194
|
+
servername: hostname, // SNI for the real server
|
|
195
|
+
}, resolve);
|
|
196
|
+
req.on('error', reject);
|
|
197
|
+
req.write(forwardBody);
|
|
198
|
+
req.end();
|
|
199
|
+
});
|
|
200
|
+
// Stream response back
|
|
201
|
+
const resHeaders = [`HTTP/1.1 ${providerRes.statusCode} ${providerRes.statusMessage}`];
|
|
202
|
+
for (const [key, val] of Object.entries(providerRes.headers)) {
|
|
203
|
+
if (val) {
|
|
204
|
+
const values = Array.isArray(val) ? val : [val];
|
|
205
|
+
for (const v of values)
|
|
206
|
+
resHeaders.push(`${key}: ${v}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
resHeaders.push('', '');
|
|
210
|
+
clientTLS.write(resHeaders.join('\r\n'));
|
|
211
|
+
await new Promise((resolve) => {
|
|
212
|
+
providerRes.on('data', (chunk) => clientTLS.write(chunk));
|
|
213
|
+
providerRes.on('end', () => { clientTLS.end(); resolve(); });
|
|
214
|
+
providerRes.on('error', () => { clientTLS.end(); resolve(); });
|
|
215
|
+
});
|
|
216
|
+
// Record metrics
|
|
217
|
+
this.options.metrics.record({
|
|
218
|
+
id: reqId,
|
|
219
|
+
timestamp: Date.now(),
|
|
220
|
+
provider: match.provider,
|
|
221
|
+
model,
|
|
222
|
+
streaming: headers['accept']?.includes('text/event-stream') || false,
|
|
223
|
+
originalTokens,
|
|
224
|
+
optimizedTokens,
|
|
225
|
+
savingsPercent,
|
|
226
|
+
latencyOverheadMs: latency,
|
|
227
|
+
chunksRetrieved: 0,
|
|
228
|
+
topScore: 0,
|
|
229
|
+
passThrough,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
exports.TransparentListener = TransparentListener;
|
|
234
|
+
/**
|
|
235
|
+
* Extract SNI hostname from TLS ClientHello.
|
|
236
|
+
* Returns null if not found.
|
|
237
|
+
*/
|
|
238
|
+
function extractSNI(data) {
|
|
239
|
+
try {
|
|
240
|
+
// TLS record: type=22 (handshake), version, length
|
|
241
|
+
if (data.length < 5 || data[0] !== 0x16)
|
|
242
|
+
return null;
|
|
243
|
+
// Handshake: type=1 (ClientHello)
|
|
244
|
+
let offset = 5;
|
|
245
|
+
if (data[offset] !== 0x01)
|
|
246
|
+
return null;
|
|
247
|
+
// Skip handshake length (3 bytes) + client version (2) + random (32)
|
|
248
|
+
offset += 4 + 2 + 32;
|
|
249
|
+
// Session ID
|
|
250
|
+
const sessionIdLen = data[offset];
|
|
251
|
+
offset += 1 + sessionIdLen;
|
|
252
|
+
// Cipher suites
|
|
253
|
+
const cipherLen = data.readUInt16BE(offset);
|
|
254
|
+
offset += 2 + cipherLen;
|
|
255
|
+
// Compression methods
|
|
256
|
+
const compLen = data[offset];
|
|
257
|
+
offset += 1 + compLen;
|
|
258
|
+
// Extensions
|
|
259
|
+
if (offset + 2 > data.length)
|
|
260
|
+
return null;
|
|
261
|
+
const extLen = data.readUInt16BE(offset);
|
|
262
|
+
offset += 2;
|
|
263
|
+
const extEnd = offset + extLen;
|
|
264
|
+
while (offset + 4 < extEnd && offset < data.length) {
|
|
265
|
+
const extType = data.readUInt16BE(offset);
|
|
266
|
+
const extDataLen = data.readUInt16BE(offset + 2);
|
|
267
|
+
offset += 4;
|
|
268
|
+
if (extType === 0x0000) { // SNI extension
|
|
269
|
+
// Skip SNI list length (2)
|
|
270
|
+
offset += 2;
|
|
271
|
+
const nameType = data[offset];
|
|
272
|
+
offset += 1;
|
|
273
|
+
if (nameType === 0x00) { // hostname
|
|
274
|
+
const nameLen = data.readUInt16BE(offset);
|
|
275
|
+
offset += 2;
|
|
276
|
+
return data.subarray(offset, offset + nameLen).toString('ascii');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
offset += extDataLen;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch { }
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
//# sourceMappingURL=transparent-listener.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Socket } from 'node:net';
|
|
2
|
+
/**
|
|
3
|
+
* Create a blind TCP tunnel between client and target.
|
|
4
|
+
* Zero overhead — no inspection, no buffering.
|
|
5
|
+
* Used for non-LLM HTTPS traffic.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createTunnel(clientSocket: Socket, targetHost: string, targetPort: number): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createTunnel = createTunnel;
|
|
7
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
8
|
+
/**
|
|
9
|
+
* Create a blind TCP tunnel between client and target.
|
|
10
|
+
* Zero overhead — no inspection, no buffering.
|
|
11
|
+
* Used for non-LLM HTTPS traffic.
|
|
12
|
+
*/
|
|
13
|
+
function createTunnel(clientSocket, targetHost, targetPort) {
|
|
14
|
+
const targetSocket = node_net_1.default.connect(targetPort, targetHost, () => {
|
|
15
|
+
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
16
|
+
targetSocket.pipe(clientSocket);
|
|
17
|
+
clientSocket.pipe(targetSocket);
|
|
18
|
+
});
|
|
19
|
+
targetSocket.on('error', (err) => {
|
|
20
|
+
clientSocket.write(`HTTP/1.1 502 Bad Gateway\r\n\r\n`);
|
|
21
|
+
clientSocket.end();
|
|
22
|
+
});
|
|
23
|
+
clientSocket.on('error', () => {
|
|
24
|
+
targetSocket.destroy();
|
|
25
|
+
});
|
|
26
|
+
clientSocket.on('close', () => {
|
|
27
|
+
targetSocket.destroy();
|
|
28
|
+
});
|
|
29
|
+
targetSocket.on('close', () => {
|
|
30
|
+
clientSocket.destroy();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=tunnel.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve and cache real IPs before overriding DNS.
|
|
3
|
+
* MUST be called before enableDNSRedirect.
|
|
4
|
+
*/
|
|
5
|
+
export declare function cacheRealIPs(): Promise<Map<string, string[]>>;
|
|
6
|
+
/** Get cached real IP for a hostname */
|
|
7
|
+
export declare function getRealIP(hostname: string): string | null;
|
|
8
|
+
/** Get all original IP mappings */
|
|
9
|
+
export declare function getAllRealIPs(): Map<string, string[]>;
|
|
10
|
+
/**
|
|
11
|
+
* Add /etc/hosts entries pointing LLM hostnames to 127.0.0.1.
|
|
12
|
+
* Requires sudo.
|
|
13
|
+
* After this, all apps resolve LLM hosts to localhost → our proxy.
|
|
14
|
+
*/
|
|
15
|
+
export declare function enableDNSRedirect(): {
|
|
16
|
+
success: boolean;
|
|
17
|
+
message: string;
|
|
18
|
+
hosts: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Remove /etc/hosts entries.
|
|
22
|
+
*/
|
|
23
|
+
export declare function disableDNSRedirect(): {
|
|
24
|
+
success: boolean;
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
/** Check if DNS redirect is active */
|
|
28
|
+
export declare function isDNSRedirectActive(): boolean;
|