@skrillex1224/playwright-toolkit 2.1.166 → 2.1.167
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/dist/browser.js +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.cjs +384 -793
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +381 -791
- package/dist/index.js.map +4 -4
- package/dist/proxy-meter.js +549 -0
- package/package.json +1 -1
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import net from 'net';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { URL } from 'url';
|
|
5
|
+
|
|
6
|
+
const HOST = '127.0.0.1';
|
|
7
|
+
const PORT = Number(process.env.PROXY_METER_PORT || 8899);
|
|
8
|
+
const LOG_PATH = String(process.env.PROXY_METER_LOG || '/tmp/proxy-meter.json');
|
|
9
|
+
const FLUSH_INTERVAL_MS = Number(process.env.PROXY_METER_FLUSH_MS || 5000);
|
|
10
|
+
const UPSTREAM_URL = String(process.env.PROXY_METER_UPSTREAM || '').trim();
|
|
11
|
+
const RUN_ID = String(process.env.PROXY_METER_RUN_ID || process.env.APIFY_ACTOR_RUN_ID || '').trim();
|
|
12
|
+
|
|
13
|
+
const isTruthy = (value) => /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
14
|
+
const DEBUG_ENABLED = isTruthy(process.env.PROXY_METER_DEBUG);
|
|
15
|
+
const DEBUG_MAX_EVENTS = Math.max(10, Number(process.env.PROXY_METER_DEBUG_MAX_EVENTS || 400));
|
|
16
|
+
const LARGE_RESPONSE_THRESHOLD_BYTES = Math.max(1024, Number(process.env.PROXY_METER_LARGE_BYTES || 1024 * 1024));
|
|
17
|
+
|
|
18
|
+
const state = {
|
|
19
|
+
startedAt: new Date().toISOString(),
|
|
20
|
+
totalInBytes: 0,
|
|
21
|
+
totalOutBytes: 0,
|
|
22
|
+
hosts: {},
|
|
23
|
+
debug: DEBUG_ENABLED
|
|
24
|
+
? {
|
|
25
|
+
enabled: true,
|
|
26
|
+
totalEvents: 0,
|
|
27
|
+
droppedEvents: 0,
|
|
28
|
+
events: [],
|
|
29
|
+
domains: {},
|
|
30
|
+
domainStatus: {},
|
|
31
|
+
}
|
|
32
|
+
: null,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getHostBucket = (host) => {
|
|
36
|
+
if (!host) return null;
|
|
37
|
+
if (!state.hosts[host]) {
|
|
38
|
+
state.hosts[host] = { inBytes: 0, outBytes: 0, connections: 0, requests: 0 };
|
|
39
|
+
}
|
|
40
|
+
return state.hosts[host];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const addTraffic = (host, dir, bytes) => {
|
|
44
|
+
const bucket = getHostBucket(host);
|
|
45
|
+
if (!bucket) return;
|
|
46
|
+
const size = Number(bytes) || 0;
|
|
47
|
+
if (size <= 0) return;
|
|
48
|
+
if (dir === 'in') {
|
|
49
|
+
bucket.inBytes += size;
|
|
50
|
+
state.totalInBytes += size;
|
|
51
|
+
} else {
|
|
52
|
+
bucket.outBytes += size;
|
|
53
|
+
state.totalOutBytes += size;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const safeError = (error) => {
|
|
58
|
+
const message = String(error?.message || error || '').trim();
|
|
59
|
+
if (!message) return '';
|
|
60
|
+
return message.length > 240 ? message.slice(0, 240) : message;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const statusLabel = (statusCode, error) => {
|
|
64
|
+
const code = Number(statusCode) || 0;
|
|
65
|
+
if (error || code <= 0) return 'ERR';
|
|
66
|
+
return `${Math.floor(code / 100)}xx`;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const ensureDebugDomain = (host) => {
|
|
70
|
+
if (!state.debug || !host) return null;
|
|
71
|
+
if (!state.debug.domains[host]) {
|
|
72
|
+
state.debug.domains[host] = {
|
|
73
|
+
domain: host,
|
|
74
|
+
count: 0,
|
|
75
|
+
failedCount: 0,
|
|
76
|
+
inBytes: 0,
|
|
77
|
+
outBytes: 0,
|
|
78
|
+
totalBytes: 0,
|
|
79
|
+
durationTotalMs: 0,
|
|
80
|
+
largeResponseCount: 0,
|
|
81
|
+
reconnectCount: 0,
|
|
82
|
+
connectSuccessCount: 0,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return state.debug.domains[host];
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const ensureDebugDomainStatus = (host, status) => {
|
|
89
|
+
if (!state.debug || !host || !status) return null;
|
|
90
|
+
const key = `${host}::${status}`;
|
|
91
|
+
if (!state.debug.domainStatus[key]) {
|
|
92
|
+
state.debug.domainStatus[key] = {
|
|
93
|
+
domain: host,
|
|
94
|
+
status,
|
|
95
|
+
count: 0,
|
|
96
|
+
failedCount: 0,
|
|
97
|
+
inBytes: 0,
|
|
98
|
+
outBytes: 0,
|
|
99
|
+
totalBytes: 0,
|
|
100
|
+
durationTotalMs: 0,
|
|
101
|
+
largeResponseCount: 0,
|
|
102
|
+
reconnectCount: 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return state.debug.domainStatus[key];
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const recordDebugEvent = (event) => {
|
|
109
|
+
if (!state.debug || !event || typeof event !== 'object') return;
|
|
110
|
+
const host = String(event.host || '').trim();
|
|
111
|
+
if (!host) return;
|
|
112
|
+
|
|
113
|
+
const code = Number(event.statusCode) || 0;
|
|
114
|
+
const inBytes = Math.max(0, Number(event.inBytes) || 0);
|
|
115
|
+
const outBytes = Math.max(0, Number(event.outBytes) || 0);
|
|
116
|
+
const totalBytes = inBytes + outBytes;
|
|
117
|
+
const durationMs = Math.max(0, Number(event.durationMs) || 0);
|
|
118
|
+
const error = String(event.error || '').trim();
|
|
119
|
+
const failed = Boolean(error) || (code > 0 && code >= 400);
|
|
120
|
+
const status = statusLabel(code, error);
|
|
121
|
+
const large = inBytes >= LARGE_RESPONSE_THRESHOLD_BYTES;
|
|
122
|
+
const channel = String(event.channel || '').trim() || 'http';
|
|
123
|
+
|
|
124
|
+
const normalized = {
|
|
125
|
+
ts: event.ts || new Date().toISOString(),
|
|
126
|
+
runId: RUN_ID,
|
|
127
|
+
channel,
|
|
128
|
+
host,
|
|
129
|
+
method: String(event.method || ''),
|
|
130
|
+
path: String(event.path || ''),
|
|
131
|
+
statusCode: code,
|
|
132
|
+
durationMs,
|
|
133
|
+
inBytes,
|
|
134
|
+
outBytes,
|
|
135
|
+
totalBytes,
|
|
136
|
+
error,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
state.debug.totalEvents += 1;
|
|
140
|
+
if (state.debug.events.length < DEBUG_MAX_EVENTS) {
|
|
141
|
+
state.debug.events.push(normalized);
|
|
142
|
+
} else {
|
|
143
|
+
state.debug.droppedEvents += 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const domain = ensureDebugDomain(host);
|
|
147
|
+
if (domain) {
|
|
148
|
+
domain.count += 1;
|
|
149
|
+
if (failed) domain.failedCount += 1;
|
|
150
|
+
domain.inBytes += inBytes;
|
|
151
|
+
domain.outBytes += outBytes;
|
|
152
|
+
domain.totalBytes += totalBytes;
|
|
153
|
+
domain.durationTotalMs += durationMs;
|
|
154
|
+
if (large) domain.largeResponseCount += 1;
|
|
155
|
+
if (channel === 'connect' && code === 200) {
|
|
156
|
+
domain.connectSuccessCount += 1;
|
|
157
|
+
if (domain.connectSuccessCount > 1) {
|
|
158
|
+
domain.reconnectCount += 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const byStatus = ensureDebugDomainStatus(host, status);
|
|
164
|
+
if (byStatus) {
|
|
165
|
+
byStatus.count += 1;
|
|
166
|
+
if (failed) byStatus.failedCount += 1;
|
|
167
|
+
byStatus.inBytes += inBytes;
|
|
168
|
+
byStatus.outBytes += outBytes;
|
|
169
|
+
byStatus.totalBytes += totalBytes;
|
|
170
|
+
byStatus.durationTotalMs += durationMs;
|
|
171
|
+
if (large) byStatus.largeResponseCount += 1;
|
|
172
|
+
if (channel === 'connect' && code === 200 && domain?.reconnectCount > 0) {
|
|
173
|
+
byStatus.reconnectCount = domain.reconnectCount;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const toDebugRow = (row) => {
|
|
179
|
+
const count = Number(row?.count) || 0;
|
|
180
|
+
const durationTotalMs = Number(row?.durationTotalMs) || 0;
|
|
181
|
+
const avgDurationMs = count > 0 ? durationTotalMs / count : 0;
|
|
182
|
+
const failedCount = Number(row?.failedCount) || 0;
|
|
183
|
+
const failureRatePct = count > 0 ? (failedCount / count) * 100 : 0;
|
|
184
|
+
return {
|
|
185
|
+
domain: String(row?.domain || ''),
|
|
186
|
+
status: String(row?.status || ''),
|
|
187
|
+
count,
|
|
188
|
+
failedCount,
|
|
189
|
+
inBytes: Math.max(0, Number(row?.inBytes) || 0),
|
|
190
|
+
outBytes: Math.max(0, Number(row?.outBytes) || 0),
|
|
191
|
+
totalBytes: Math.max(0, Number(row?.totalBytes) || 0),
|
|
192
|
+
avgDurationMs: Math.max(0, avgDurationMs),
|
|
193
|
+
reconnectCount: Math.max(0, Number(row?.reconnectCount) || 0),
|
|
194
|
+
largeResponseCount: Math.max(0, Number(row?.largeResponseCount) || 0),
|
|
195
|
+
failureRatePct,
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const buildDebugSummary = () => {
|
|
200
|
+
if (!state.debug) return null;
|
|
201
|
+
const domainRows = Object.values(state.debug.domains || {}).map(toDebugRow);
|
|
202
|
+
const domainStatus = Object.values(state.debug.domainStatus || {}).map(toDebugRow);
|
|
203
|
+
|
|
204
|
+
const requestCount = domainRows.reduce((sum, row) => sum + row.count, 0);
|
|
205
|
+
const failedCount = domainRows.reduce((sum, row) => sum + row.failedCount, 0);
|
|
206
|
+
const largeResponseCount = domainRows.reduce((sum, row) => sum + row.largeResponseCount, 0);
|
|
207
|
+
const reconnectCount = domainRows.reduce((sum, row) => sum + row.reconnectCount, 0);
|
|
208
|
+
const failureRatePct = requestCount > 0 ? (failedCount / requestCount) * 100 : 0;
|
|
209
|
+
|
|
210
|
+
const topFailureDomains = [...domainRows]
|
|
211
|
+
.filter((row) => row.failedCount > 0)
|
|
212
|
+
.sort((a, b) => {
|
|
213
|
+
if (b.failedCount !== a.failedCount) return b.failedCount - a.failedCount;
|
|
214
|
+
return b.totalBytes - a.totalBytes;
|
|
215
|
+
})
|
|
216
|
+
.slice(0, 20);
|
|
217
|
+
const topLargeDomains = [...domainRows]
|
|
218
|
+
.filter((row) => row.largeResponseCount > 0)
|
|
219
|
+
.sort((a, b) => {
|
|
220
|
+
if (b.largeResponseCount !== a.largeResponseCount) return b.largeResponseCount - a.largeResponseCount;
|
|
221
|
+
return b.totalBytes - a.totalBytes;
|
|
222
|
+
})
|
|
223
|
+
.slice(0, 20);
|
|
224
|
+
const topReconnectDomains = [...domainRows]
|
|
225
|
+
.filter((row) => row.reconnectCount > 0)
|
|
226
|
+
.sort((a, b) => {
|
|
227
|
+
if (b.reconnectCount !== a.reconnectCount) return b.reconnectCount - a.reconnectCount;
|
|
228
|
+
return b.totalBytes - a.totalBytes;
|
|
229
|
+
})
|
|
230
|
+
.slice(0, 20);
|
|
231
|
+
|
|
232
|
+
domainStatus.sort((a, b) => {
|
|
233
|
+
if (b.totalBytes !== a.totalBytes) return b.totalBytes - a.totalBytes;
|
|
234
|
+
return b.count - a.count;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
largeResponseThresholdBytes: LARGE_RESPONSE_THRESHOLD_BYTES,
|
|
239
|
+
requestCount,
|
|
240
|
+
failedCount,
|
|
241
|
+
failureRatePct,
|
|
242
|
+
largeResponseCount,
|
|
243
|
+
reconnectCount,
|
|
244
|
+
domainStatus: domainStatus.slice(0, 200),
|
|
245
|
+
topFailureDomains,
|
|
246
|
+
topLargeDomains,
|
|
247
|
+
topReconnectDomains,
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const flushSnapshot = () => {
|
|
252
|
+
try {
|
|
253
|
+
const snapshot = {
|
|
254
|
+
startedAt: state.startedAt,
|
|
255
|
+
totalInBytes: state.totalInBytes,
|
|
256
|
+
totalOutBytes: state.totalOutBytes,
|
|
257
|
+
hosts: state.hosts,
|
|
258
|
+
updatedAt: new Date().toISOString(),
|
|
259
|
+
};
|
|
260
|
+
if (state.debug) {
|
|
261
|
+
snapshot.debug = {
|
|
262
|
+
enabled: true,
|
|
263
|
+
totalEvents: state.debug.totalEvents,
|
|
264
|
+
sampledEvents: state.debug.events.length,
|
|
265
|
+
droppedEvents: state.debug.droppedEvents,
|
|
266
|
+
events: state.debug.events,
|
|
267
|
+
summary: buildDebugSummary(),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
fs.writeFileSync(LOG_PATH, JSON.stringify(snapshot, null, 2));
|
|
271
|
+
} catch {}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const parseUpstream = () => {
|
|
275
|
+
if (!UPSTREAM_URL) return null;
|
|
276
|
+
let parsed;
|
|
277
|
+
try {
|
|
278
|
+
parsed = new URL(UPSTREAM_URL);
|
|
279
|
+
} catch {
|
|
280
|
+
throw new Error(`[proxy-meter] invalid upstream url: ${UPSTREAM_URL}`);
|
|
281
|
+
}
|
|
282
|
+
if (parsed.protocol !== 'http:') {
|
|
283
|
+
throw new Error(`[proxy-meter] upstream protocol not supported: ${parsed.protocol}`);
|
|
284
|
+
}
|
|
285
|
+
const authHeader = parsed.username
|
|
286
|
+
? `Basic ${Buffer.from(`${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password || '')}`).toString('base64')}`
|
|
287
|
+
: '';
|
|
288
|
+
return {
|
|
289
|
+
host: parsed.hostname,
|
|
290
|
+
port: Number(parsed.port) || 80,
|
|
291
|
+
authHeader,
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const upstream = parseUpstream();
|
|
296
|
+
|
|
297
|
+
const resolveTarget = (req) => {
|
|
298
|
+
try {
|
|
299
|
+
if (req.url && req.url.startsWith('http')) {
|
|
300
|
+
return new URL(req.url);
|
|
301
|
+
}
|
|
302
|
+
const host = req.headers.host || '';
|
|
303
|
+
return new URL(`http://${host}${req.url || '/'}`);
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const createRequestTracker = (base) => {
|
|
310
|
+
const startedAt = Date.now();
|
|
311
|
+
let inBytes = 0;
|
|
312
|
+
let outBytes = 0;
|
|
313
|
+
let closed = false;
|
|
314
|
+
return {
|
|
315
|
+
addIn: (bytes) => {
|
|
316
|
+
inBytes += Math.max(0, Number(bytes) || 0);
|
|
317
|
+
},
|
|
318
|
+
addOut: (bytes) => {
|
|
319
|
+
outBytes += Math.max(0, Number(bytes) || 0);
|
|
320
|
+
},
|
|
321
|
+
close: (extra = {}) => {
|
|
322
|
+
if (closed) return;
|
|
323
|
+
closed = true;
|
|
324
|
+
recordDebugEvent({
|
|
325
|
+
ts: new Date().toISOString(),
|
|
326
|
+
runId: RUN_ID,
|
|
327
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
328
|
+
inBytes,
|
|
329
|
+
outBytes,
|
|
330
|
+
totalBytes: inBytes + outBytes,
|
|
331
|
+
...base,
|
|
332
|
+
...extra,
|
|
333
|
+
});
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const forwardHttp = (req, res) => {
|
|
339
|
+
const target = resolveTarget(req);
|
|
340
|
+
const fallbackHost = String(req.headers?.host || '').split(':')[0].trim();
|
|
341
|
+
const hostname = target?.hostname || fallbackHost || '(unknown)';
|
|
342
|
+
const path = target ? `${target.pathname}${target.search || ''}` : String(req.url || '/');
|
|
343
|
+
const tracker = createRequestTracker({
|
|
344
|
+
channel: 'http',
|
|
345
|
+
host: hostname,
|
|
346
|
+
method: String(req.method || 'GET'),
|
|
347
|
+
path,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
if (!target) {
|
|
351
|
+
tracker.close({ statusCode: 0, error: 'invalid_target_url' });
|
|
352
|
+
res.writeHead(502);
|
|
353
|
+
res.end('Bad Gateway');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const bucket = getHostBucket(hostname);
|
|
358
|
+
if (bucket) bucket.requests += 1;
|
|
359
|
+
|
|
360
|
+
const headers = { ...req.headers };
|
|
361
|
+
if (upstream && upstream.authHeader && !headers['proxy-authorization']) {
|
|
362
|
+
headers['proxy-authorization'] = upstream.authHeader;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const requestOptions = upstream
|
|
366
|
+
? {
|
|
367
|
+
hostname: upstream.host,
|
|
368
|
+
port: upstream.port,
|
|
369
|
+
method: req.method,
|
|
370
|
+
path: target.href,
|
|
371
|
+
headers,
|
|
372
|
+
}
|
|
373
|
+
: {
|
|
374
|
+
hostname: target.hostname,
|
|
375
|
+
port: Number(target.port) || 80,
|
|
376
|
+
method: req.method,
|
|
377
|
+
path: `${target.pathname}${target.search || ''}`,
|
|
378
|
+
headers,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
let responseStatus = 0;
|
|
382
|
+
const proxyReq = http.request(requestOptions, (proxyRes) => {
|
|
383
|
+
responseStatus = Number(proxyRes.statusCode) || 0;
|
|
384
|
+
res.writeHead(responseStatus || 502, proxyRes.headers);
|
|
385
|
+
proxyRes.on('data', (chunk) => {
|
|
386
|
+
const size = chunk?.length || 0;
|
|
387
|
+
addTraffic(hostname, 'in', size);
|
|
388
|
+
tracker.addIn(size);
|
|
389
|
+
});
|
|
390
|
+
proxyRes.on('end', () => {
|
|
391
|
+
tracker.close({ statusCode: responseStatus });
|
|
392
|
+
});
|
|
393
|
+
proxyRes.on('error', (error) => {
|
|
394
|
+
tracker.close({ statusCode: responseStatus, error: safeError(error) });
|
|
395
|
+
});
|
|
396
|
+
proxyRes.pipe(res);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
req.on('data', (chunk) => {
|
|
400
|
+
const size = chunk?.length || 0;
|
|
401
|
+
addTraffic(hostname, 'out', size);
|
|
402
|
+
tracker.addOut(size);
|
|
403
|
+
});
|
|
404
|
+
req.on('aborted', () => {
|
|
405
|
+
tracker.close({ statusCode: responseStatus || 499, error: 'request_aborted' });
|
|
406
|
+
});
|
|
407
|
+
req.pipe(proxyReq);
|
|
408
|
+
|
|
409
|
+
res.on('close', () => {
|
|
410
|
+
tracker.close({ statusCode: responseStatus || 499 });
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
proxyReq.on('error', (error) => {
|
|
414
|
+
tracker.close({ statusCode: responseStatus, error: safeError(error) });
|
|
415
|
+
res.writeHead(502);
|
|
416
|
+
res.end('Bad Gateway');
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const forwardConnect = (req, clientSocket, head) => {
|
|
421
|
+
const [host, portText] = String(req.url || '').split(':');
|
|
422
|
+
const hostname = host || '(unknown)';
|
|
423
|
+
const port = Number(portText) || 443;
|
|
424
|
+
const tracker = createRequestTracker({
|
|
425
|
+
channel: 'connect',
|
|
426
|
+
host: hostname,
|
|
427
|
+
method: 'CONNECT',
|
|
428
|
+
path: `${hostname}:${port}`,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const bucket = getHostBucket(hostname);
|
|
432
|
+
if (bucket) bucket.connections += 1;
|
|
433
|
+
|
|
434
|
+
let established = false;
|
|
435
|
+
let responseStatus = 0;
|
|
436
|
+
|
|
437
|
+
const onError = (socketA, socketB) => (error) => {
|
|
438
|
+
tracker.close({ statusCode: responseStatus, error: safeError(error) });
|
|
439
|
+
try { socketA.destroy(); } catch {}
|
|
440
|
+
try { socketB.destroy(); } catch {}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const attachTrafficHooks = (upstreamSocket) => {
|
|
444
|
+
upstreamSocket.on('data', (chunk) => {
|
|
445
|
+
const size = chunk?.length || 0;
|
|
446
|
+
addTraffic(hostname, 'in', size);
|
|
447
|
+
tracker.addIn(size);
|
|
448
|
+
});
|
|
449
|
+
clientSocket.on('data', (chunk) => {
|
|
450
|
+
const size = chunk?.length || 0;
|
|
451
|
+
addTraffic(hostname, 'out', size);
|
|
452
|
+
tracker.addOut(size);
|
|
453
|
+
});
|
|
454
|
+
clientSocket.on('close', () => {
|
|
455
|
+
tracker.close({
|
|
456
|
+
statusCode: responseStatus || (established ? 200 : 0),
|
|
457
|
+
error: established ? '' : 'connect_closed',
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
if (upstream) {
|
|
463
|
+
const upstreamSocket = net.connect(upstream.port, upstream.host, () => {
|
|
464
|
+
const headers = [
|
|
465
|
+
`CONNECT ${hostname}:${port} HTTP/1.1`,
|
|
466
|
+
`Host: ${hostname}:${port}`,
|
|
467
|
+
];
|
|
468
|
+
if (upstream.authHeader) {
|
|
469
|
+
headers.push(`Proxy-Authorization: ${upstream.authHeader}`);
|
|
470
|
+
}
|
|
471
|
+
headers.push('\r\n');
|
|
472
|
+
upstreamSocket.write(headers.join('\r\n'));
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
let responseBuffer = Buffer.alloc(0);
|
|
476
|
+
const onHandshake = (chunk) => {
|
|
477
|
+
responseBuffer = Buffer.concat([responseBuffer, chunk]);
|
|
478
|
+
const headerEnd = responseBuffer.indexOf('\r\n\r\n');
|
|
479
|
+
if (headerEnd === -1) return;
|
|
480
|
+
upstreamSocket.off('data', onHandshake);
|
|
481
|
+
const headerText = responseBuffer.slice(0, headerEnd).toString('utf8');
|
|
482
|
+
const statusLine = headerText.split('\r\n')[0] || '';
|
|
483
|
+
responseStatus = Number((statusLine.split(' ')[1] || '').trim());
|
|
484
|
+
if (responseStatus !== 200) {
|
|
485
|
+
tracker.close({ statusCode: responseStatus, error: `upstream_connect_${responseStatus || 0}` });
|
|
486
|
+
clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
|
|
487
|
+
clientSocket.destroy();
|
|
488
|
+
upstreamSocket.destroy();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
established = true;
|
|
493
|
+
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
494
|
+
const rest = responseBuffer.slice(headerEnd + 4);
|
|
495
|
+
if (rest.length) {
|
|
496
|
+
addTraffic(hostname, 'in', rest.length);
|
|
497
|
+
tracker.addIn(rest.length);
|
|
498
|
+
clientSocket.write(rest);
|
|
499
|
+
}
|
|
500
|
+
if (head && head.length) {
|
|
501
|
+
addTraffic(hostname, 'out', head.length);
|
|
502
|
+
tracker.addOut(head.length);
|
|
503
|
+
upstreamSocket.write(head);
|
|
504
|
+
}
|
|
505
|
+
clientSocket.pipe(upstreamSocket);
|
|
506
|
+
upstreamSocket.pipe(clientSocket);
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
attachTrafficHooks(upstreamSocket);
|
|
510
|
+
upstreamSocket.on('data', onHandshake);
|
|
511
|
+
clientSocket.on('error', onError(clientSocket, upstreamSocket));
|
|
512
|
+
upstreamSocket.on('error', onError(clientSocket, upstreamSocket));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const serverSocket = net.connect(port, hostname, () => {
|
|
517
|
+
established = true;
|
|
518
|
+
responseStatus = 200;
|
|
519
|
+
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
520
|
+
if (head && head.length) {
|
|
521
|
+
addTraffic(hostname, 'out', head.length);
|
|
522
|
+
tracker.addOut(head.length);
|
|
523
|
+
serverSocket.write(head);
|
|
524
|
+
}
|
|
525
|
+
clientSocket.pipe(serverSocket);
|
|
526
|
+
serverSocket.pipe(clientSocket);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
attachTrafficHooks(serverSocket);
|
|
530
|
+
clientSocket.on('error', onError(clientSocket, serverSocket));
|
|
531
|
+
serverSocket.on('error', onError(clientSocket, serverSocket));
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const server = http.createServer(forwardHttp);
|
|
535
|
+
server.on('connect', forwardConnect);
|
|
536
|
+
server.listen(PORT, HOST, () => {
|
|
537
|
+
console.log(`[proxy-meter] listening ${HOST}:${PORT} log=${LOG_PATH}`);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
setInterval(flushSnapshot, FLUSH_INTERVAL_MS).unref();
|
|
541
|
+
flushSnapshot();
|
|
542
|
+
|
|
543
|
+
const shutdown = () => {
|
|
544
|
+
flushSnapshot();
|
|
545
|
+
process.exit(0);
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
process.on('SIGINT', shutdown);
|
|
549
|
+
process.on('SIGTERM', shutdown);
|