@jonathanleelx/meddle 0.4.1-beta.2 → 0.4.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/bin/lib/crash-report.js +192 -0
- package/dist/core/connect-tunnel.d.ts.map +1 -1
- package/dist/core/connect-tunnel.js +8 -2
- package/dist/core/connect-tunnel.js.map +1 -1
- package/dist/core/h2-pool.d.ts +3 -0
- package/dist/core/h2-pool.d.ts.map +1 -1
- package/dist/core/h2-pool.js +38 -7
- package/dist/core/h2-pool.js.map +1 -1
- package/index.js +49 -5
- package/package.json +1 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* meddle crash-report — crash dump collection with fingerprinting and dedupe.
|
|
3
|
+
*
|
|
4
|
+
* Pure JS so it runs unmodified in the npm package and in the deno-compiled
|
|
5
|
+
* binary (bin/ is bundled at compile time). Never throws: the crash reporter
|
|
6
|
+
* must not crash while reporting a crash.
|
|
7
|
+
*
|
|
8
|
+
* Layout: <home>/crash/<fingerprint>-<timestamp>.json
|
|
9
|
+
*
|
|
10
|
+
* Privacy: only env KEY names are recorded, never their values; request
|
|
11
|
+
* headers/URLs are scrubbed of common credential patterns before writing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs')
|
|
15
|
+
const os = require('os')
|
|
16
|
+
const path = require('path')
|
|
17
|
+
|
|
18
|
+
const CRASH_DIR = 'crash'
|
|
19
|
+
const MAX_FILES_PER_FINGERPRINT = 10
|
|
20
|
+
|
|
21
|
+
const SENSITIVE_HEADER_PATTERNS = [
|
|
22
|
+
/authorization/i,
|
|
23
|
+
/cookie/i,
|
|
24
|
+
/set-cookie/i,
|
|
25
|
+
/token/i,
|
|
26
|
+
/session/i,
|
|
27
|
+
/x-api-key/i,
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const SENSITIVE_QUERY_PATTERNS = [
|
|
31
|
+
/token=/i,
|
|
32
|
+
/key=/i,
|
|
33
|
+
/secret=/i,
|
|
34
|
+
/password=/i,
|
|
35
|
+
/apikey=/i,
|
|
36
|
+
/appid=/i,
|
|
37
|
+
/app_id=/i,
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Normalize a stack frame into a stable token:
|
|
42
|
+
* - file:///.../dist/core/h2-pool.js:182:30 → h2-pool.js
|
|
43
|
+
* - ext:deno_node/net.ts:1:21858 → net.ts
|
|
44
|
+
* - node:internal/timers.mjs:4:1174 → internal/timers.mjs
|
|
45
|
+
* - (file:///x/dist/a.js:10:20) → a.js
|
|
46
|
+
* Line/column numbers and volatile absolute paths are stripped so identical
|
|
47
|
+
* crashes (different addresses/timestamps) share one fingerprint.
|
|
48
|
+
*/
|
|
49
|
+
function normalizeFrame(frame) {
|
|
50
|
+
const line = frame.trim()
|
|
51
|
+
const match = line.match(/\(?(?:file:\/\/)?[^)\s]*[\\/]([^\\/)\s:]+\.(?:js|ts|mjs|cjs))(?::\d+:\d+)?\)?/)
|
|
52
|
+
if (match) return match[1]
|
|
53
|
+
// bare frames like "at f (a.js:10:20)" or "at TCPWrap.onStreamRead"
|
|
54
|
+
const bare = line.match(/at\s+([\w.]+)(?:\s*\(.*\))?$/)
|
|
55
|
+
if (bare) return bare[1]
|
|
56
|
+
return line.slice(0, 80)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build a stable fingerprint for an error. Prefers the error `code` when
|
|
61
|
+
* present (e.g. ECONNRESET); otherwise the first stack frame chain is
|
|
62
|
+
* normalized to remove addresses, paths and line numbers.
|
|
63
|
+
* @param {Error & { code?: string }} err
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
function crashFingerprint(err) {
|
|
67
|
+
if (err && typeof err.code === 'string' && err.code) return err.code
|
|
68
|
+
if (err && typeof err.stack === 'string') {
|
|
69
|
+
const lines = err.stack.split('\n').slice(0, 5)
|
|
70
|
+
const frames = lines.map(normalizeFrame).filter(Boolean)
|
|
71
|
+
if (frames.length > 0) return frames.join('|').slice(0, 120)
|
|
72
|
+
}
|
|
73
|
+
if (err && err.message) return String(err.message).slice(0, 120)
|
|
74
|
+
return 'unknown'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Strip credential-shaped values from a free-form object (headers, URL query).
|
|
79
|
+
* Returns a new object with sensitive keys replaced by a marker.
|
|
80
|
+
*/
|
|
81
|
+
function scrubSensitive(input) {
|
|
82
|
+
if (!input || typeof input !== 'object') return input
|
|
83
|
+
const out = {}
|
|
84
|
+
for (const [key, value] of Object.entries(input)) {
|
|
85
|
+
const k = String(key)
|
|
86
|
+
const isSensitiveHeader = SENSITIVE_HEADER_PATTERNS.some(p => p.test(k))
|
|
87
|
+
const isSensitiveQuery = SENSITIVE_QUERY_PATTERNS.some(p => p.test(k))
|
|
88
|
+
if (isSensitiveHeader || isSensitiveQuery) {
|
|
89
|
+
out[k] = '[redacted]'
|
|
90
|
+
} else {
|
|
91
|
+
out[k] = value
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Sanitize crash context before it is written to disk.
|
|
99
|
+
* Only env key names are kept (never values); request metadata is scrubbed.
|
|
100
|
+
* @param {{ envKeys?: string[], requestUrl?: string, headers?: Record<string, string> }} input
|
|
101
|
+
* @returns {Record<string, unknown>}
|
|
102
|
+
*/
|
|
103
|
+
function sanitizeCrashData(input) {
|
|
104
|
+
const out = {}
|
|
105
|
+
if (Array.isArray(input.envKeys)) {
|
|
106
|
+
out.envKeys = input.envKeys.filter(k => typeof k === 'string')
|
|
107
|
+
}
|
|
108
|
+
if (typeof input.requestUrl === 'string') {
|
|
109
|
+
try {
|
|
110
|
+
const u = new URL(input.requestUrl)
|
|
111
|
+
out.requestUrl = u.origin + u.pathname
|
|
112
|
+
if (u.search) {
|
|
113
|
+
const params = new URLSearchParams(u.search)
|
|
114
|
+
const scrubbed = {}
|
|
115
|
+
for (const [k, v] of params) {
|
|
116
|
+
scrubbed[k] = SENSITIVE_QUERY_PATTERNS.some(p => p.test(k + '=')) ? '[redacted]' : v
|
|
117
|
+
}
|
|
118
|
+
out.requestQuery = scrubbed
|
|
119
|
+
}
|
|
120
|
+
} catch (_) {
|
|
121
|
+
out.requestUrl = '[invalid-url]'
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (input.headers && typeof input.headers === 'object') {
|
|
125
|
+
out.headers = scrubSensitive(input.headers)
|
|
126
|
+
}
|
|
127
|
+
return out
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Record a crash dump to <home>/crash/<fingerprint>-<ts>.json.
|
|
132
|
+
* Dedupes by fingerprint (max MAX_FILES_PER_FINGERPRINT). Never throws.
|
|
133
|
+
* @param {{ home: string, error: Error, version?: string, platform?: string,
|
|
134
|
+
* envKeys?: string[], requestUrl?: string, headers?: Record<string, string>,
|
|
135
|
+
* context?: Record<string, unknown>, now?: number }} opts
|
|
136
|
+
* @returns {string} the written file path (or '' when the write failed)
|
|
137
|
+
*/
|
|
138
|
+
function recordCrash(opts) {
|
|
139
|
+
try {
|
|
140
|
+
const home = opts.home
|
|
141
|
+
const error = opts.error || new Error('unknown')
|
|
142
|
+
const now = opts.now !== undefined ? opts.now : Date.now()
|
|
143
|
+
const fingerprint = crashFingerprint(error)
|
|
144
|
+
const crashDir = path.join(home, CRASH_DIR)
|
|
145
|
+
fs.mkdirSync(crashDir, { recursive: true })
|
|
146
|
+
|
|
147
|
+
// Dedupe: evict oldest files of the same fingerprint beyond the cap.
|
|
148
|
+
const existing = fs.readdirSync(crashDir)
|
|
149
|
+
.filter(f => f.startsWith(fingerprint + '-'))
|
|
150
|
+
.sort()
|
|
151
|
+
while (existing.length >= MAX_FILES_PER_FINGERPRINT) {
|
|
152
|
+
const oldest = existing.shift()
|
|
153
|
+
if (oldest) {
|
|
154
|
+
try { fs.unlinkSync(path.join(crashDir, oldest)) } catch (_) {}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const sanitized = sanitizeCrashData({
|
|
159
|
+
envKeys: opts.envKeys,
|
|
160
|
+
requestUrl: opts.requestUrl,
|
|
161
|
+
headers: opts.headers,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const data = {
|
|
165
|
+
fingerprint,
|
|
166
|
+
timestamp: now,
|
|
167
|
+
version: opts.version || '',
|
|
168
|
+
platform: opts.platform || `${os.platform()}-${os.arch()}`,
|
|
169
|
+
error: {
|
|
170
|
+
name: error.name || 'Error',
|
|
171
|
+
message: error.message || String(error),
|
|
172
|
+
stack: typeof error.stack === 'string' ? error.stack : String(error),
|
|
173
|
+
},
|
|
174
|
+
...sanitized,
|
|
175
|
+
context: opts.context || {},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const file = path.join(crashDir, `${fingerprint}-${now}.json`)
|
|
179
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8')
|
|
180
|
+
return file
|
|
181
|
+
} catch (_) {
|
|
182
|
+
return ''
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
crashFingerprint,
|
|
188
|
+
sanitizeCrashData,
|
|
189
|
+
recordCrash,
|
|
190
|
+
CRASH_DIR,
|
|
191
|
+
MAX_FILES_PER_FINGERPRINT,
|
|
192
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect-tunnel.d.ts","sourceRoot":"","sources":["../../core/connect-tunnel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAA;AAEjC,eAAO,MAAM,4BAA4B,uEAC+B,CAAA;AAExE,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAI7D;AAED,wBAAgB,sBAAsB,CAClC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,MAAqC,GAChD,IAAI,
|
|
1
|
+
{"version":3,"file":"connect-tunnel.d.ts","sourceRoot":"","sources":["../../core/connect-tunnel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAA;AAEjC,eAAO,MAAM,4BAA4B,uEAC+B,CAAA;AAExE,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAI7D;AAED,wBAAgB,sBAAsB,CAClC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,MAAqC,GAChD,IAAI,CAsCN"}
|
|
@@ -34,8 +34,14 @@ function establishConnectTunnel(client, upstream, head, response = exports.CONNE
|
|
|
34
34
|
}
|
|
35
35
|
client.once('close', closeBoth);
|
|
36
36
|
upstream.once('close', closeBoth);
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Persistent error listeners: a tunnel socket can emit 'error' more
|
|
38
|
+
// than once (e.g. an RST while the other side is still draining). With
|
|
39
|
+
// `once` the listener would be consumed by the first error and a second
|
|
40
|
+
// emission would crash the process via uncaughtException (observed in
|
|
41
|
+
// the deno binary). `on` keeps the bridge protected for the socket's
|
|
42
|
+
// lifetime; closeBoth is idempotent.
|
|
43
|
+
client.on('error', closeBoth);
|
|
44
|
+
upstream.on('error', closeBoth);
|
|
39
45
|
if (head.length > 0)
|
|
40
46
|
upstream.write(head);
|
|
41
47
|
client.pipe(upstream);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect-tunnel.js","sourceRoot":"","sources":["../../core/connect-tunnel.ts"],"names":[],"mappings":";;;AAKA,sDAIC;AAED,
|
|
1
|
+
{"version":3,"file":"connect-tunnel.js","sourceRoot":"","sources":["../../core/connect-tunnel.ts"],"names":[],"mappings":";;;AAKA,sDAIC;AAED,wDA2CC;AApDY,QAAA,4BAA4B,GACrC,oEAAoE,CAAA;AAExE,SAAgB,qBAAqB,CAAC,KAAc;IAChD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,IAAI,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAC3E,MAAM,IAAI,GAAI,KAA+B,CAAC,IAAI,CAAA;IAClD,OAAO,IAAI,KAAK,YAAY,IAAI,IAAI,KAAK,OAAO,CAAA;AACpD,CAAC;AAED,SAAgB,sBAAsB,CAClC,MAAc,EACd,QAAgB,EAChB,IAAY,EACZ,WAAmB,oCAA4B;IAE/C,IAAI,MAAM,GAAG,KAAK,CAAA;IAClB,MAAM,SAAS,GAAG,GAAS,EAAE;QACzB,IAAI,MAAM;YAAE,OAAM;QAClB,MAAM,GAAG,IAAI,CAAA;QAEb,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;QACvB,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACvB,IAAI,CAAC,MAAM,CAAC,SAAS;YAAE,MAAM,CAAC,OAAO,EAAE,CAAA;QACvC,IAAI,CAAC,QAAQ,CAAC,SAAS;YAAE,QAAQ,CAAC,OAAO,EAAE,CAAA;IAC/C,CAAC,CAAA;IAED,IAAI,MAAM,CAAC,SAAS,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC;QACzC,SAAS,EAAE,CAAA;QACX,OAAM;IACV,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,IAAI,MAAM,CAAC,SAAS,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC;YACzC,SAAS,EAAE,CAAA;YACX,OAAM;QACV,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;QAC/B,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;QACjC,oEAAoE;QACpE,uEAAuE;QACvE,wEAAwE;QACxE,sEAAsE;QACtE,qEAAqE;QACrE,qCAAqC;QACrC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;QAC7B,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;QAE/B,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACzC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACrB,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACzB,CAAC,CAAC,CAAA;AACN,CAAC"}
|
package/dist/core/h2-pool.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import * as http2 from 'http2';
|
|
1
2
|
import type { ProxyResponse } from './types';
|
|
3
|
+
export declare const UPSTREAM_TIMEOUT_CODE = "UPSTREAM_TIMEOUT";
|
|
2
4
|
export declare function cleanHeadersForH2(headers: Record<string, any>): Record<string, any>;
|
|
5
|
+
export declare function __setH2SessionFactoryForTest(factory: ((origin: string, opts: http2.SecureClientSessionOptions) => http2.ClientHttp2Session) | null): void;
|
|
3
6
|
export declare function makeProxyRequest(target: string, method: string, headers: Record<string, any>, reqBody: Buffer): Promise<ProxyResponse>;
|
|
4
7
|
//# sourceMappingURL=h2-pool.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"h2-pool.d.ts","sourceRoot":"","sources":["../../core/h2-pool.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"h2-pool.d.ts","sourceRoot":"","sources":["../../core/h2-pool.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAc5C,eAAO,MAAM,qBAAqB,qBAAqB,CAAA;AA2BvD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CASnF;AAKD,wBAAgB,4BAA4B,CACxC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,0BAA0B,KAAK,KAAK,CAAC,kBAAkB,CAAC,GAAG,IAAI,GACvG,IAAI,CAEN;AA2HD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAU5I"}
|
package/dist/core/h2-pool.js
CHANGED
|
@@ -36,7 +36,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.UPSTREAM_TIMEOUT_CODE = void 0;
|
|
39
40
|
exports.cleanHeadersForH2 = cleanHeadersForH2;
|
|
41
|
+
exports.__setH2SessionFactoryForTest = __setH2SessionFactoryForTest;
|
|
40
42
|
exports.makeProxyRequest = makeProxyRequest;
|
|
41
43
|
const http = __importStar(require("http"));
|
|
42
44
|
const https = __importStar(require("https"));
|
|
@@ -45,7 +47,18 @@ const debug_1 = __importDefault(require("debug"));
|
|
|
45
47
|
const proxyDebug = (0, debug_1.default)('proxy');
|
|
46
48
|
const h2SessionPool = new Map();
|
|
47
49
|
const MAX_H2_SESSIONS = process.env.MEDDLE_MAX_H2_SESSIONS ? parseInt(process.env.MEDDLE_MAX_H2_SESSIONS) : 32;
|
|
48
|
-
const
|
|
50
|
+
const DEFAULT_UPSTREAM_REQUEST_TIMEOUT_MS = 60000;
|
|
51
|
+
function upstreamTimeoutMs() {
|
|
52
|
+
const fromEnv = process.env.MEDDLE_UPSTREAM_TIMEOUT_MS;
|
|
53
|
+
const parsed = fromEnv ? parseInt(fromEnv, 10) : NaN;
|
|
54
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_UPSTREAM_REQUEST_TIMEOUT_MS;
|
|
55
|
+
}
|
|
56
|
+
exports.UPSTREAM_TIMEOUT_CODE = 'UPSTREAM_TIMEOUT';
|
|
57
|
+
function upstreamTimeoutError(target, timeoutMs) {
|
|
58
|
+
const err = new Error(`上游连接超时: ${timeoutMs / 1000}s 内未收到 ${target} 的响应(上游服务可能挂起、网络不通或规则指向了不可达目标)`);
|
|
59
|
+
err.code = exports.UPSTREAM_TIMEOUT_CODE;
|
|
60
|
+
return err;
|
|
61
|
+
}
|
|
49
62
|
function evictOldestH2SessionIfNeeded() {
|
|
50
63
|
while (h2SessionPool.size >= MAX_H2_SESSIONS) {
|
|
51
64
|
const oldestKey = h2SessionPool.keys().next().value;
|
|
@@ -80,6 +93,15 @@ function cleanHeadersForH2(headers) {
|
|
|
80
93
|
}
|
|
81
94
|
return cleaned;
|
|
82
95
|
}
|
|
96
|
+
// Test-only hook: override the session constructor so specs can inject a fake
|
|
97
|
+
// http2 session (e.g. one that emits 'error' repeatedly). Do not call from prod.
|
|
98
|
+
let sessionFactoryOverride = null;
|
|
99
|
+
function __setH2SessionFactoryForTest(factory) {
|
|
100
|
+
sessionFactoryOverride = factory;
|
|
101
|
+
}
|
|
102
|
+
function createH2Session(origin, opts) {
|
|
103
|
+
return sessionFactoryOverride ? sessionFactoryOverride(origin, opts) : http2.connect(origin, opts);
|
|
104
|
+
}
|
|
83
105
|
function getOrCreateH2Session(origin, servername) {
|
|
84
106
|
const poolKey = servername ? `${origin}#${servername}` : origin;
|
|
85
107
|
const cached = h2SessionPool.get(poolKey);
|
|
@@ -91,10 +113,12 @@ function getOrCreateH2Session(origin, servername) {
|
|
|
91
113
|
const connectOpts = { rejectUnauthorized: false };
|
|
92
114
|
if (servername)
|
|
93
115
|
connectOpts.servername = servername;
|
|
94
|
-
const session =
|
|
116
|
+
const session = createH2Session(origin, connectOpts);
|
|
95
117
|
const timeout = setTimeout(() => {
|
|
96
118
|
session.destroy();
|
|
97
|
-
|
|
119
|
+
const err = new Error(`HTTP/2 连接超时: 5s 内未能与 ${origin} 建立 HTTP/2 连接(上游不支持 H2 或网络不通)`);
|
|
120
|
+
err.code = 'H2_CONNECT_TIMEOUT';
|
|
121
|
+
reject(err);
|
|
98
122
|
}, 5000);
|
|
99
123
|
session.once('connect', () => {
|
|
100
124
|
clearTimeout(timeout);
|
|
@@ -102,10 +126,17 @@ function getOrCreateH2Session(origin, servername) {
|
|
|
102
126
|
h2SessionPool.set(poolKey, session);
|
|
103
127
|
resolve(session);
|
|
104
128
|
});
|
|
105
|
-
session
|
|
129
|
+
// Persistent error listener: a session that errored once must be
|
|
130
|
+
// evicted AND destroyed so it is never reused. Using `once` would drop
|
|
131
|
+
// the listener after the first error — a second error (e.g. an RST
|
|
132
|
+
// while a concurrent caller still holds the session) had no listener
|
|
133
|
+
// and crashed the whole process via uncaughtException.
|
|
134
|
+
session.on('error', (err) => {
|
|
106
135
|
clearTimeout(timeout);
|
|
107
136
|
h2SessionPool.delete(poolKey);
|
|
108
|
-
|
|
137
|
+
if (!session.destroyed)
|
|
138
|
+
session.destroy();
|
|
139
|
+
reject(err); // no-op after the promise has settled
|
|
109
140
|
});
|
|
110
141
|
session.on('close', () => { h2SessionPool.delete(poolKey); });
|
|
111
142
|
session.on('goaway', () => {
|
|
@@ -178,8 +209,8 @@ function proxyViaH1(target, method, headers, reqBody) {
|
|
|
178
209
|
protocol: 'h1.1'
|
|
179
210
|
});
|
|
180
211
|
});
|
|
181
|
-
proxyReq.setTimeout(
|
|
182
|
-
proxyReq.destroy(
|
|
212
|
+
proxyReq.setTimeout(upstreamTimeoutMs(), () => {
|
|
213
|
+
proxyReq.destroy(upstreamTimeoutError(target, upstreamTimeoutMs()));
|
|
183
214
|
});
|
|
184
215
|
proxyReq.on('error', reject);
|
|
185
216
|
if (reqBody && reqBody.length > 0)
|
package/dist/core/h2-pool.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"h2-pool.js","sourceRoot":"","sources":["../../core/h2-pool.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"h2-pool.js","sourceRoot":"","sources":["../../core/h2-pool.ts"],"names":[],"mappings":"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"}
|
package/index.js
CHANGED
|
@@ -22,6 +22,8 @@ const {
|
|
|
22
22
|
} = require('./dist/core/client-identity')
|
|
23
23
|
const { createApplicationIdentityResolver } = require('./dist/core/application-identity')
|
|
24
24
|
const { runAsyncUpdateCheck } = require('./bin/lib/update-check')
|
|
25
|
+
const { resolveMeddleHome } = require('./bin/lib/meddle-home')
|
|
26
|
+
const { recordCrash, crashFingerprint } = require('./bin/lib/crash-report')
|
|
25
27
|
const {
|
|
26
28
|
authorizeProxyClient,
|
|
27
29
|
buildRemoteAccessConfig,
|
|
@@ -37,7 +39,10 @@ const chalk = require('chalk')
|
|
|
37
39
|
const _debug = require('debug')
|
|
38
40
|
|
|
39
41
|
const proxyDebug = _debug('proxy')
|
|
40
|
-
const UPSTREAM_REQUEST_TIMEOUT_MS =
|
|
42
|
+
const UPSTREAM_REQUEST_TIMEOUT_MS = process.env.MEDDLE_UPSTREAM_TIMEOUT_MS
|
|
43
|
+
? parseInt(process.env.MEDDLE_UPSTREAM_TIMEOUT_MS, 10)
|
|
44
|
+
: 60000
|
|
45
|
+
const UPSTREAM_TIMEOUT_CODE = 'UPSTREAM_TIMEOUT'
|
|
41
46
|
const MITM_SERVER_IDLE_TTL_MS = 10 * 60 * 1000
|
|
42
47
|
const MITM_SERVER_SWEEP_INTERVAL_MS = 60 * 1000
|
|
43
48
|
const MAX_WS_BUFFERED_MESSAGES = 1000
|
|
@@ -463,11 +468,17 @@ const proxyServer = http.createServer(async (req, res) => {
|
|
|
463
468
|
})
|
|
464
469
|
})
|
|
465
470
|
proxyReq.setTimeout(UPSTREAM_REQUEST_TIMEOUT_MS, () => {
|
|
466
|
-
|
|
471
|
+
const err = new Error(`上游连接超时: ${UPSTREAM_REQUEST_TIMEOUT_MS / 1000}s 内未收到 ${url.href} 的响应`)
|
|
472
|
+
err.code = UPSTREAM_TIMEOUT_CODE
|
|
473
|
+
proxyReq.destroy(err)
|
|
467
474
|
})
|
|
468
475
|
proxyReq.on('error', (err) => {
|
|
469
476
|
pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
|
|
470
|
-
|
|
477
|
+
if (err && err.code === UPSTREAM_TIMEOUT_CODE) {
|
|
478
|
+
console.warn(`[proxy:upstream-timeout] ${getErrorMessage(err)}`)
|
|
479
|
+
} else {
|
|
480
|
+
logRuntimeError(`http:proxy-request:${getErrorKey(err)}`, 'HTTP proxy error:', getErrorMessage(err))
|
|
481
|
+
}
|
|
471
482
|
finishResponseWithProxyError(res)
|
|
472
483
|
})
|
|
473
484
|
proxyReq.write(reqBody)
|
|
@@ -719,10 +730,12 @@ proxyServer.on('connect', async (req, socket, head) => {
|
|
|
719
730
|
} catch (err) {
|
|
720
731
|
const code = err.code || ''
|
|
721
732
|
const isConnReset = code === 'ECONNRESET' || code === 'ETIMEDOUT' || code === 'ECONNREFUSED'
|
|
722
|
-
if (
|
|
733
|
+
if (code === UPSTREAM_TIMEOUT_CODE) {
|
|
734
|
+
console.warn(`[proxy:upstream-timeout] ${getErrorMessage(err)} → ${originHost}${req.url}`)
|
|
735
|
+
} else if (isConnReset) {
|
|
723
736
|
logRuntimeError(`https:upstream:${originHost}:${code}`, '[proxy] upstream %s %s: %s', originHost + req.url, code, err.message)
|
|
724
737
|
} else {
|
|
725
|
-
logRuntimeError(`https:upstream:${originHost}:${getErrorKey(err)}`, '[
|
|
738
|
+
logRuntimeError(`https:upstream:${originHost}:${getErrorKey(err)}`, '[proxy] upstream %s: %s', originHost + req.url, getErrorMessage(err))
|
|
726
739
|
}
|
|
727
740
|
pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
|
|
728
741
|
finishResponseWithProxyError(res)
|
|
@@ -737,6 +750,15 @@ proxyServer.on('connect', async (req, socket, head) => {
|
|
|
737
750
|
markMitmServerUsed(server)
|
|
738
751
|
server._epActiveSockets = new Set()
|
|
739
752
|
|
|
753
|
+
// A client resetting its TLS connection (ECONNRESET) can surface
|
|
754
|
+
// as an 'error' on the http2 server. Without a listener this
|
|
755
|
+
// would propagate to uncaughtException and kill the whole proxy.
|
|
756
|
+
server.on('error', (err) => {
|
|
757
|
+
if (!isExpectedSocketError(err)) {
|
|
758
|
+
proxyDebug('MITM server error', originHost, getErrorMessage(err))
|
|
759
|
+
}
|
|
760
|
+
})
|
|
761
|
+
|
|
740
762
|
server._epRegisterClientIdentity = (remotePort, identity) => {
|
|
741
763
|
clientIdentityRegistry.register(remotePort, identity)
|
|
742
764
|
}
|
|
@@ -957,6 +979,28 @@ function handleFatalError(type, reason) {
|
|
|
957
979
|
if (!fatalErrorInProgress) {
|
|
958
980
|
fatalErrorInProgress = true
|
|
959
981
|
writeFatalError(type, reason)
|
|
982
|
+
|
|
983
|
+
// 崩溃采集: 落盘现场(指纹去重、脱敏),便于事后定位
|
|
984
|
+
const err = reason instanceof Error ? reason : new Error(String(reason))
|
|
985
|
+
try {
|
|
986
|
+
const home = resolveMeddleHome()
|
|
987
|
+
const crashFile = recordCrash({
|
|
988
|
+
home,
|
|
989
|
+
error: err,
|
|
990
|
+
version: require('./package.json').version,
|
|
991
|
+
envKeys: Object.keys(process.env || {}),
|
|
992
|
+
context: {
|
|
993
|
+
fatalType: type,
|
|
994
|
+
connections: activeProxySockets.size,
|
|
995
|
+
mitmServers: ctx.httpsServerMap ? ctx.httpsServerMap.size : 0,
|
|
996
|
+
},
|
|
997
|
+
})
|
|
998
|
+
if (crashFile) {
|
|
999
|
+
console.error(`[fatal] 崩溃报告已保存: ${crashFile} (指纹 ${crashFingerprint(err)})`)
|
|
1000
|
+
}
|
|
1001
|
+
} catch (_) {
|
|
1002
|
+
// 崩溃报告失败不影响退出
|
|
1003
|
+
}
|
|
960
1004
|
}
|
|
961
1005
|
|
|
962
1006
|
process.exit(1)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jonathanleelx/meddle",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "A simple development proxy server with custom routing rules, mock responses, HTTPS interception, and a plugin system",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "index.js",
|