@jonathanleelx/meddle 0.0.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.
Files changed (361) hide show
  1. package/CONFIG_STRUCTURE.md +315 -0
  2. package/LICENSE +21 -0
  3. package/README.md +298 -0
  4. package/bin/commands/mock/add.js +92 -0
  5. package/bin/commands/mock/delete.js +58 -0
  6. package/bin/commands/mock/index.js +50 -0
  7. package/bin/commands/mock/list.js +68 -0
  8. package/bin/commands/mock/toggle.js +64 -0
  9. package/bin/commands/mock/update.js +94 -0
  10. package/bin/commands/route/active.js +141 -0
  11. package/bin/commands/route/add.js +127 -0
  12. package/bin/commands/route/create.js +77 -0
  13. package/bin/commands/route/delete.js +120 -0
  14. package/bin/commands/route/index.js +59 -0
  15. package/bin/commands/route/list.js +67 -0
  16. package/bin/commands/route/preview.js +159 -0
  17. package/bin/commands/route/show.js +111 -0
  18. package/bin/commands/route/update.js +126 -0
  19. package/bin/commands/session/create.js +166 -0
  20. package/bin/commands/session/delete.js +69 -0
  21. package/bin/commands/session/index.js +61 -0
  22. package/bin/commands/session/list.js +42 -0
  23. package/bin/commands/session/prune.js +31 -0
  24. package/bin/commands/start.js +43 -0
  25. package/bin/commands/status.js +115 -0
  26. package/bin/commands/supervise.js +106 -0
  27. package/bin/commands/url.js +30 -0
  28. package/bin/doctor.js +356 -0
  29. package/bin/index +130 -0
  30. package/bin/lib/api-client.js +72 -0
  31. package/bin/lib/file-access.js +337 -0
  32. package/bin/lib/meddle-home.js +20 -0
  33. package/bin/lib/output.js +146 -0
  34. package/bin/lib/parsers.js +127 -0
  35. package/bin/lib/proxy-detect.js +105 -0
  36. package/bin/lib/route-preview.js +180 -0
  37. package/bin/lib/session-args.js +54 -0
  38. package/bin/lib/sessions.js +213 -0
  39. package/dist/cert.d.ts +10 -0
  40. package/dist/cert.d.ts.map +1 -0
  41. package/dist/cert.js +184 -0
  42. package/dist/cert.js.map +1 -0
  43. package/dist/core/application-identity.d.ts +55 -0
  44. package/dist/core/application-identity.d.ts.map +1 -0
  45. package/dist/core/application-identity.js +321 -0
  46. package/dist/core/application-identity.js.map +1 -0
  47. package/dist/core/body-utils.d.ts +2 -0
  48. package/dist/core/body-utils.d.ts.map +1 -0
  49. package/dist/core/body-utils.js +21 -0
  50. package/dist/core/body-utils.js.map +1 -0
  51. package/dist/core/browser.d.ts +2 -0
  52. package/dist/core/browser.d.ts.map +1 -0
  53. package/dist/core/browser.js +92 -0
  54. package/dist/core/browser.js.map +1 -0
  55. package/dist/core/client-identity.d.ts +27 -0
  56. package/dist/core/client-identity.d.ts.map +1 -0
  57. package/dist/core/client-identity.js +136 -0
  58. package/dist/core/client-identity.js.map +1 -0
  59. package/dist/core/config-diagnostics.d.ts +14 -0
  60. package/dist/core/config-diagnostics.d.ts.map +1 -0
  61. package/dist/core/config-diagnostics.js +137 -0
  62. package/dist/core/config-diagnostics.js.map +1 -0
  63. package/dist/core/connect-tunnel.d.ts +5 -0
  64. package/dist/core/connect-tunnel.d.ts.map +1 -0
  65. package/dist/core/connect-tunnel.js +45 -0
  66. package/dist/core/connect-tunnel.js.map +1 -0
  67. package/dist/core/content-encoding.d.ts +2 -0
  68. package/dist/core/content-encoding.d.ts.map +1 -0
  69. package/dist/core/content-encoding.js +77 -0
  70. package/dist/core/content-encoding.js.map +1 -0
  71. package/dist/core/custom-plugin-loader.d.ts +18 -0
  72. package/dist/core/custom-plugin-loader.d.ts.map +1 -0
  73. package/dist/core/custom-plugin-loader.js +178 -0
  74. package/dist/core/custom-plugin-loader.js.map +1 -0
  75. package/dist/core/h2-pool.d.ts +4 -0
  76. package/dist/core/h2-pool.d.ts.map +1 -0
  77. package/dist/core/h2-pool.js +180 -0
  78. package/dist/core/h2-pool.js.map +1 -0
  79. package/dist/core/inspection-dispatch.d.ts +5 -0
  80. package/dist/core/inspection-dispatch.d.ts.map +1 -0
  81. package/dist/core/inspection-dispatch.js +146 -0
  82. package/dist/core/inspection-dispatch.js.map +1 -0
  83. package/dist/core/log-rate-limit.d.ts +34 -0
  84. package/dist/core/log-rate-limit.d.ts.map +1 -0
  85. package/dist/core/log-rate-limit.js +52 -0
  86. package/dist/core/log-rate-limit.js.map +1 -0
  87. package/dist/core/map-local.d.ts +4 -0
  88. package/dist/core/map-local.d.ts.map +1 -0
  89. package/dist/core/map-local.js +195 -0
  90. package/dist/core/map-local.js.map +1 -0
  91. package/dist/core/meddle-home.d.ts +13 -0
  92. package/dist/core/meddle-home.d.ts.map +1 -0
  93. package/dist/core/meddle-home.js +56 -0
  94. package/dist/core/meddle-home.js.map +1 -0
  95. package/dist/core/mock-gate.d.ts +3 -0
  96. package/dist/core/mock-gate.d.ts.map +1 -0
  97. package/dist/core/mock-gate.js +20 -0
  98. package/dist/core/mock-gate.js.map +1 -0
  99. package/dist/core/mock-handler.d.ts +5 -0
  100. package/dist/core/mock-handler.d.ts.map +1 -0
  101. package/dist/core/mock-handler.js +339 -0
  102. package/dist/core/mock-handler.js.map +1 -0
  103. package/dist/core/on-mode-gate.d.ts +4 -0
  104. package/dist/core/on-mode-gate.d.ts.map +1 -0
  105. package/dist/core/on-mode-gate.js +64 -0
  106. package/dist/core/on-mode-gate.js.map +1 -0
  107. package/dist/core/pipeline-gate.d.ts +3 -0
  108. package/dist/core/pipeline-gate.d.ts.map +1 -0
  109. package/dist/core/pipeline-gate.js +34 -0
  110. package/dist/core/pipeline-gate.js.map +1 -0
  111. package/dist/core/pipeline.d.ts +6 -0
  112. package/dist/core/pipeline.d.ts.map +1 -0
  113. package/dist/core/pipeline.js +228 -0
  114. package/dist/core/pipeline.js.map +1 -0
  115. package/dist/core/plugin-bootstrap-runner.d.ts +6 -0
  116. package/dist/core/plugin-bootstrap-runner.d.ts.map +1 -0
  117. package/dist/core/plugin-bootstrap-runner.js +148 -0
  118. package/dist/core/plugin-bootstrap-runner.js.map +1 -0
  119. package/dist/core/plugin-bootstrap.d.ts +3 -0
  120. package/dist/core/plugin-bootstrap.d.ts.map +1 -0
  121. package/dist/core/plugin-bootstrap.js +15 -0
  122. package/dist/core/plugin-bootstrap.js.map +1 -0
  123. package/dist/core/plugin-compiler.d.ts +37 -0
  124. package/dist/core/plugin-compiler.d.ts.map +1 -0
  125. package/dist/core/plugin-compiler.js +172 -0
  126. package/dist/core/plugin-compiler.js.map +1 -0
  127. package/dist/core/plugin-context-factory.d.ts +29 -0
  128. package/dist/core/plugin-context-factory.d.ts.map +1 -0
  129. package/dist/core/plugin-context-factory.js +206 -0
  130. package/dist/core/plugin-context-factory.js.map +1 -0
  131. package/dist/core/plugin-generator.d.ts +47 -0
  132. package/dist/core/plugin-generator.d.ts.map +1 -0
  133. package/dist/core/plugin-generator.js +697 -0
  134. package/dist/core/plugin-generator.js.map +1 -0
  135. package/dist/core/plugin-health.d.ts +3 -0
  136. package/dist/core/plugin-health.d.ts.map +1 -0
  137. package/dist/core/plugin-health.js +54 -0
  138. package/dist/core/plugin-health.js.map +1 -0
  139. package/dist/core/plugin-intercept.d.ts +17 -0
  140. package/dist/core/plugin-intercept.d.ts.map +1 -0
  141. package/dist/core/plugin-intercept.js +133 -0
  142. package/dist/core/plugin-intercept.js.map +1 -0
  143. package/dist/core/plugin-runtime.d.ts +33 -0
  144. package/dist/core/plugin-runtime.d.ts.map +1 -0
  145. package/dist/core/plugin-runtime.js +254 -0
  146. package/dist/core/plugin-runtime.js.map +1 -0
  147. package/dist/core/proxy-context.d.ts +3 -0
  148. package/dist/core/proxy-context.d.ts.map +1 -0
  149. package/dist/core/proxy-context.js +129 -0
  150. package/dist/core/proxy-context.js.map +1 -0
  151. package/dist/core/proxy-record.d.ts +3 -0
  152. package/dist/core/proxy-record.d.ts.map +1 -0
  153. package/dist/core/proxy-record.js +32 -0
  154. package/dist/core/proxy-record.js.map +1 -0
  155. package/dist/core/refactor-config.d.ts +6 -0
  156. package/dist/core/refactor-config.d.ts.map +1 -0
  157. package/dist/core/refactor-config.js +45 -0
  158. package/dist/core/refactor-config.js.map +1 -0
  159. package/dist/core/refactor-status.d.ts +3 -0
  160. package/dist/core/refactor-status.d.ts.map +1 -0
  161. package/dist/core/refactor-status.js +19 -0
  162. package/dist/core/refactor-status.js.map +1 -0
  163. package/dist/core/remote-access.d.ts +40 -0
  164. package/dist/core/remote-access.d.ts.map +1 -0
  165. package/dist/core/remote-access.js +281 -0
  166. package/dist/core/remote-access.js.map +1 -0
  167. package/dist/core/route-decision.d.ts +3 -0
  168. package/dist/core/route-decision.d.ts.map +1 -0
  169. package/dist/core/route-decision.js +20 -0
  170. package/dist/core/route-decision.js.map +1 -0
  171. package/dist/core/route-loader.d.ts +14 -0
  172. package/dist/core/route-loader.d.ts.map +1 -0
  173. package/dist/core/route-loader.js +94 -0
  174. package/dist/core/route-loader.js.map +1 -0
  175. package/dist/core/route-preview.d.ts +15 -0
  176. package/dist/core/route-preview.d.ts.map +1 -0
  177. package/dist/core/route-preview.js +91 -0
  178. package/dist/core/route-preview.js.map +1 -0
  179. package/dist/core/runtime-health.d.ts +92 -0
  180. package/dist/core/runtime-health.d.ts.map +1 -0
  181. package/dist/core/runtime-health.js +205 -0
  182. package/dist/core/runtime-health.js.map +1 -0
  183. package/dist/core/sessions.d.ts +19 -0
  184. package/dist/core/sessions.d.ts.map +1 -0
  185. package/dist/core/sessions.js +93 -0
  186. package/dist/core/sessions.js.map +1 -0
  187. package/dist/core/shadow-compare.d.ts +8 -0
  188. package/dist/core/shadow-compare.d.ts.map +1 -0
  189. package/dist/core/shadow-compare.js +83 -0
  190. package/dist/core/shadow-compare.js.map +1 -0
  191. package/dist/core/shadow-readiness.d.ts +4 -0
  192. package/dist/core/shadow-readiness.d.ts.map +1 -0
  193. package/dist/core/shadow-readiness.js +118 -0
  194. package/dist/core/shadow-readiness.js.map +1 -0
  195. package/dist/core/short-response.d.ts +5 -0
  196. package/dist/core/short-response.d.ts.map +1 -0
  197. package/dist/core/short-response.js +18 -0
  198. package/dist/core/short-response.js.map +1 -0
  199. package/dist/core/static-server.d.ts +9 -0
  200. package/dist/core/static-server.d.ts.map +1 -0
  201. package/dist/core/static-server.js +136 -0
  202. package/dist/core/static-server.js.map +1 -0
  203. package/dist/core/types.d.ts +590 -0
  204. package/dist/core/types.d.ts.map +1 -0
  205. package/dist/core/types.js +4 -0
  206. package/dist/core/types.js.map +1 -0
  207. package/dist/helpers.d.ts +36 -0
  208. package/dist/helpers.d.ts.map +1 -0
  209. package/dist/helpers.js +290 -0
  210. package/dist/helpers.js.map +1 -0
  211. package/dist/plugins/builtin/index.d.ts +3 -0
  212. package/dist/plugins/builtin/index.d.ts.map +1 -0
  213. package/dist/plugins/builtin/index.js +23 -0
  214. package/dist/plugins/builtin/index.js.map +1 -0
  215. package/dist/plugins/builtin/logger-plugin.d.ts +8 -0
  216. package/dist/plugins/builtin/logger-plugin.d.ts.map +1 -0
  217. package/dist/plugins/builtin/logger-plugin.js +121 -0
  218. package/dist/plugins/builtin/logger-plugin.js.map +1 -0
  219. package/dist/plugins/builtin/mock-plugin.d.ts +3 -0
  220. package/dist/plugins/builtin/mock-plugin.d.ts.map +1 -0
  221. package/dist/plugins/builtin/mock-plugin.js +99 -0
  222. package/dist/plugins/builtin/mock-plugin.js.map +1 -0
  223. package/dist/plugins/builtin/router-plugin.d.ts +3 -0
  224. package/dist/plugins/builtin/router-plugin.d.ts.map +1 -0
  225. package/dist/plugins/builtin/router-plugin.js +36 -0
  226. package/dist/plugins/builtin/router-plugin.js.map +1 -0
  227. package/dist/server/agent/model.d.ts +30 -0
  228. package/dist/server/agent/model.d.ts.map +1 -0
  229. package/dist/server/agent/model.js +143 -0
  230. package/dist/server/agent/model.js.map +1 -0
  231. package/dist/server/agent/routes.d.ts +4 -0
  232. package/dist/server/agent/routes.d.ts.map +1 -0
  233. package/dist/server/agent/routes.js +188 -0
  234. package/dist/server/agent/routes.js.map +1 -0
  235. package/dist/server/agent/runtime.d.ts +12 -0
  236. package/dist/server/agent/runtime.d.ts.map +1 -0
  237. package/dist/server/agent/runtime.js +208 -0
  238. package/dist/server/agent/runtime.js.map +1 -0
  239. package/dist/server/agent/tools.d.ts +13 -0
  240. package/dist/server/agent/tools.d.ts.map +1 -0
  241. package/dist/server/agent/tools.js +1201 -0
  242. package/dist/server/agent/tools.js.map +1 -0
  243. package/dist/server/agent/types.d.ts +56 -0
  244. package/dist/server/agent/types.d.ts.map +1 -0
  245. package/dist/server/agent/types.js +3 -0
  246. package/dist/server/agent/types.js.map +1 -0
  247. package/dist/server/config.d.ts +9 -0
  248. package/dist/server/config.d.ts.map +1 -0
  249. package/dist/server/config.js +124 -0
  250. package/dist/server/config.js.map +1 -0
  251. package/dist/server/health.d.ts +4 -0
  252. package/dist/server/health.d.ts.map +1 -0
  253. package/dist/server/health.js +22 -0
  254. package/dist/server/health.js.map +1 -0
  255. package/dist/server/index.d.ts +239 -0
  256. package/dist/server/index.d.ts.map +1 -0
  257. package/dist/server/index.js +44 -0
  258. package/dist/server/index.js.map +1 -0
  259. package/dist/server/logs.d.ts +4 -0
  260. package/dist/server/logs.d.ts.map +1 -0
  261. package/dist/server/logs.js +40 -0
  262. package/dist/server/logs.js.map +1 -0
  263. package/dist/server/mocks.d.ts +9 -0
  264. package/dist/server/mocks.d.ts.map +1 -0
  265. package/dist/server/mocks.js +103 -0
  266. package/dist/server/mocks.js.map +1 -0
  267. package/dist/server/pipeline.d.ts +16 -0
  268. package/dist/server/pipeline.d.ts.map +1 -0
  269. package/dist/server/pipeline.js +159 -0
  270. package/dist/server/pipeline.js.map +1 -0
  271. package/dist/server/plugins.d.ts +32 -0
  272. package/dist/server/plugins.d.ts.map +1 -0
  273. package/dist/server/plugins.js +795 -0
  274. package/dist/server/plugins.js.map +1 -0
  275. package/dist/server/refactor.d.ts +4 -0
  276. package/dist/server/refactor.d.ts.map +1 -0
  277. package/dist/server/refactor.js +86 -0
  278. package/dist/server/refactor.js.map +1 -0
  279. package/dist/server/rule-files.d.ts +34 -0
  280. package/dist/server/rule-files.d.ts.map +1 -0
  281. package/dist/server/rule-files.js +389 -0
  282. package/dist/server/rule-files.js.map +1 -0
  283. package/dist/server/rules.d.ts +4 -0
  284. package/dist/server/rules.d.ts.map +1 -0
  285. package/dist/server/rules.js +40 -0
  286. package/dist/server/rules.js.map +1 -0
  287. package/dist/server/sessions.d.ts +4 -0
  288. package/dist/server/sessions.d.ts.map +1 -0
  289. package/dist/server/sessions.js +20 -0
  290. package/dist/server/sessions.js.map +1 -0
  291. package/index.js +950 -0
  292. package/mcp-server.js +794 -0
  293. package/package.json +92 -0
  294. package/plugins/builtin/index.ts +25 -0
  295. package/plugins/builtin/logger-plugin.ts +126 -0
  296. package/plugins/builtin/mock-plugin.ts +112 -0
  297. package/plugins/builtin/router-plugin.ts +35 -0
  298. package/web/README.md +111 -0
  299. package/web/dist/assets/ai-config-store-DQXVJVn_.js +1 -0
  300. package/web/dist/assets/alert-COF-DIAE.js +1 -0
  301. package/web/dist/assets/arrow-right-B4Mrlj2-.js +1 -0
  302. package/web/dist/assets/babel-DsprIpXZ.js +15 -0
  303. package/web/dist/assets/body-diff-view-D7dCWajG.js +6 -0
  304. package/web/dist/assets/button-BTToMaBW.js +1 -0
  305. package/web/dist/assets/checkbox-DIBarrJx.js +1 -0
  306. package/web/dist/assets/chevron-down-CPngAx4M.js +1 -0
  307. package/web/dist/assets/chevron-right-V1DGIDbL.js +1 -0
  308. package/web/dist/assets/circle-alert-Q8cZk9Rv.js +1 -0
  309. package/web/dist/assets/circle-x-C9USji_R.js +1 -0
  310. package/web/dist/assets/code-5_eJ_XpJ.js +1 -0
  311. package/web/dist/assets/code-fixer-fQtWZ5He.js +20 -0
  312. package/web/dist/assets/copy-BYjq1hdN.js +1 -0
  313. package/web/dist/assets/detail-panel-BX_hxeI0.js +3 -0
  314. package/web/dist/assets/dist-B8XUKgXe.js +1 -0
  315. package/web/dist/assets/dist-Bb9TeEub.js +1 -0
  316. package/web/dist/assets/dist-C5nMErq8.js +1 -0
  317. package/web/dist/assets/dist-C6eaYCUo.js +1 -0
  318. package/web/dist/assets/dist-Cn0y0IRn.js +1 -0
  319. package/web/dist/assets/dist-CxdY_A67.js +5 -0
  320. package/web/dist/assets/dist-D5Dj17P8.js +1 -0
  321. package/web/dist/assets/dist-DZJ9SMlJ.js +1 -0
  322. package/web/dist/assets/es2015-CfXu88Y7.js +41 -0
  323. package/web/dist/assets/estree-BP7k0NIt.js +44 -0
  324. package/web/dist/assets/file-system-access-CtcvBfj4.js +1 -0
  325. package/web/dist/assets/global-panel-shell-ChdO0dWd.js +6 -0
  326. package/web/dist/assets/health-panel-CQeCQ7gr.js +1 -0
  327. package/web/dist/assets/html-6FIWbqxX.js +25 -0
  328. package/web/dist/assets/index-CjqaTnE_.css +2 -0
  329. package/web/dist/assets/index-L7uWxTnp.js +18 -0
  330. package/web/dist/assets/mobile-proxy-panel-Dj1iLami.js +8 -0
  331. package/web/dist/assets/mock-config-BTTCw7RK.js +1 -0
  332. package/web/dist/assets/mock-editor-panel-C-aVfFme.js +1 -0
  333. package/web/dist/assets/monaco-editor-B3brLg8j.js +11 -0
  334. package/web/dist/assets/pencil-line-Cqe41E4h.js +1 -0
  335. package/web/dist/assets/plugin-code-editor-CPjCbJ9g.js +2 -0
  336. package/web/dist/assets/plugin-config-BzmCkvz3.js +1 -0
  337. package/web/dist/assets/plugin-generator-BnEafDF9.js +2 -0
  338. package/web/dist/assets/plugin-test-dialog-BWoWsWV4.js +7 -0
  339. package/web/dist/assets/plus-tVxSydJ8.js +1 -0
  340. package/web/dist/assets/postcss-Dnf30fQU.js +61 -0
  341. package/web/dist/assets/refresh-cw-CG3HaiQT.js +1 -0
  342. package/web/dist/assets/rotate-ccw-CAYM6Vv0.js +1 -0
  343. package/web/dist/assets/route-preview-BX7sfY0O.js +1 -0
  344. package/web/dist/assets/rule-ai-assistant-panel-WBiOOUw5.js +47 -0
  345. package/web/dist/assets/rule-config-CHpVij2M.css +1 -0
  346. package/web/dist/assets/rule-config-fTIZVwg1.js +15 -0
  347. package/web/dist/assets/select-CkFOB61l.js +1 -0
  348. package/web/dist/assets/server-Cpj4KNwN.js +1 -0
  349. package/web/dist/assets/settings-panel-COCYiIUg.js +1 -0
  350. package/web/dist/assets/shield-check-OqUmVLQt.js +1 -0
  351. package/web/dist/assets/smartphone-DKeDuJTU.js +1 -0
  352. package/web/dist/assets/standalone-CjxtLKw-.js +29 -0
  353. package/web/dist/assets/switch-C-Xk56TK.js +1 -0
  354. package/web/dist/assets/syntax-highlight-DVhPdkuV.js +5 -0
  355. package/web/dist/assets/table-CT97lAYq.js +1 -0
  356. package/web/dist/assets/textarea-C6NooVK1.js +1 -0
  357. package/web/dist/assets/wand-sparkles-wkul5vje.js +1 -0
  358. package/web/dist/assets/wifi-D9M35Ajj.js +1 -0
  359. package/web/dist/assets/zap-Djo4cgux.js +1 -0
  360. package/web/dist/index.html +24 -0
  361. package/web/dist/vite.svg +1 -0
package/index.js ADDED
@@ -0,0 +1,950 @@
1
+ const http = require('http')
2
+ const http2 = require('http2')
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const { connect } = require('net')
6
+ const { WebSocket, WebSocketServer } = require('ws')
7
+ const { resolveTargetUrl, getFreePort } = require('./dist/helpers')
8
+ const { crtMgr, ensureRootCA, getRootCAPath } = require('./dist/cert')
9
+ const { decideRoute } = require('./dist/core/route-decision')
10
+ const { sendShortResponse } = require('./dist/core/short-response')
11
+ const { safeBodyToString } = require('./dist/core/body-utils')
12
+ const {
13
+ establishConnectTunnel,
14
+ isExpectedSocketError,
15
+ } = require('./dist/core/connect-tunnel')
16
+ const {
17
+ createClientIdentityResolver,
18
+ createMitmClientIdentityRegistry,
19
+ enrichClientIdentityWithApplication,
20
+ getRequestClientIdentity,
21
+ pluginClientIdentity,
22
+ } = require('./dist/core/client-identity')
23
+ const { createApplicationIdentityResolver } = require('./dist/core/application-identity')
24
+ const {
25
+ authorizeProxyClient,
26
+ buildRemoteAccessConfig,
27
+ buildRemoteSetupHtml,
28
+ createRemoteAccessInfo,
29
+ getLanIPv4Addresses,
30
+ isLoopbackAddress,
31
+ isProxyHost,
32
+ parseConnectAuthority,
33
+ stripProxyHeaders,
34
+ } = require('./dist/core/remote-access')
35
+ const chalk = require('chalk')
36
+ const _debug = require('debug')
37
+
38
+ const proxyDebug = _debug('proxy')
39
+ const UPSTREAM_REQUEST_TIMEOUT_MS = 60000
40
+ const MITM_SERVER_IDLE_TTL_MS = 10 * 60 * 1000
41
+ const MITM_SERVER_SWEEP_INTERVAL_MS = 60 * 1000
42
+ const MAX_WS_BUFFERED_MESSAGES = 1000
43
+
44
+ function getErrorMessage(err) {
45
+ return err && err.message ? err.message : String(err)
46
+ }
47
+
48
+ function getErrorKey(err) {
49
+ return err && err.code ? err.code : getErrorMessage(err)
50
+ }
51
+
52
+ function finishResponseWithProxyError(res, statusCode = 502) {
53
+ try {
54
+ if (res.destroyed || res.writableEnded) return
55
+ if (!res.headersSent) {
56
+ res.writeHead(statusCode)
57
+ res.end()
58
+ return
59
+ }
60
+ if (typeof res.destroy === 'function') {
61
+ res.destroy()
62
+ } else {
63
+ res.end()
64
+ }
65
+ } catch (_) {}
66
+ }
67
+
68
+ function markMitmServerUsed(server) {
69
+ server._epLastUsedAt = Date.now()
70
+ }
71
+
72
+ function closeIdleMitmServer(originHost, server) {
73
+ if (ctx.httpsServerMap.get(originHost) !== server) return
74
+
75
+ ctx.httpsServerMap.delete(originHost)
76
+ try { server._epWSServer?.close() } catch (_) {}
77
+ try {
78
+ server.close((err) => {
79
+ if (err) proxyDebug('MITM server close error', originHost, err.message)
80
+ })
81
+ } catch (err) {
82
+ proxyDebug('MITM server close failed', originHost, getErrorMessage(err))
83
+ }
84
+ }
85
+
86
+ function sweepIdleMitmServers() {
87
+ const now = Date.now()
88
+ for (const [originHost, server] of ctx.httpsServerMap.entries()) {
89
+ const lastUsedAt = server._epLastUsedAt || now
90
+ const activeSockets = server._epActiveSockets?.size || 0
91
+ if (activeSockets === 0 && now - lastUsedAt >= MITM_SERVER_IDLE_TTL_MS) {
92
+ closeIdleMitmServer(originHost, server)
93
+ }
94
+ }
95
+ }
96
+
97
+ // ===== 共享上下文 =====
98
+ const { createProxyContext } = require('./dist/core/proxy-context')
99
+ const ctx = createProxyContext()
100
+ const mitmServerSweepTimer = setInterval(sweepIdleMitmServers, MITM_SERVER_SWEEP_INTERVAL_MS)
101
+ mitmServerSweepTimer.unref?.()
102
+
103
+ // ===== 模块初始化 =====
104
+ const { cleanHeadersForH2, makeProxyRequest } = require('./dist/core/h2-pool')
105
+ const { createMockHandler } = require('./dist/core/mock-handler')
106
+ const { handleMapLocalRequest } = require('./dist/core/map-local')
107
+ const { createRouteLoader } = require('./dist/core/route-loader')
108
+ const { createPluginIntercept } = require('./dist/core/plugin-intercept')
109
+ const { createPluginBootstrapRunner } = require('./dist/core/plugin-bootstrap-runner')
110
+ const { openBrowserWithProxy } = require('./dist/core/browser')
111
+ const { handleLocalRequest } = require('./dist/core/static-server')
112
+ const { createConfigDiagnostics } = require('./dist/core/config-diagnostics')
113
+ const { appendProxyRecord } = require('./dist/core/proxy-record')
114
+ const { createRuntimeHealthMonitor, parseWatchdogConfig } = require('./dist/core/runtime-health')
115
+ const { createRateLimitedLogger } = require('./dist/core/log-rate-limit')
116
+
117
+ const remoteAccess = buildRemoteAccessConfig()
118
+ const lanAddresses = getLanIPv4Addresses()
119
+ const clientIdentityResolver = createClientIdentityResolver(ctx.settingsPath)
120
+ const applicationIdentityResolver = createApplicationIdentityResolver()
121
+ const activeProxySockets = new Set()
122
+ const rateLimitedLogger = createRateLimitedLogger(console, {
123
+ windowMs: Number(process.env.MEDDLE_LOG_RATE_LIMIT_WINDOW_MS) || 60000,
124
+ maxPerWindow: Number(process.env.MEDDLE_LOG_RATE_LIMIT_MAX) || 20,
125
+ })
126
+
127
+ const mockHandler = createMockHandler(ctx)
128
+ const pluginIntercept = createPluginIntercept(ctx)
129
+ const pluginBoot = createPluginBootstrapRunner(ctx, mockHandler)
130
+ const runtimeHealth = createRuntimeHealthMonitor({
131
+ getSnapshotInput: () => {
132
+ const mitmServers = Array.from(ctx.httpsServerMap.entries()).map(([host, server]) => {
133
+ const address = server.address && server.address()
134
+ const port = address && typeof address === 'object' ? address.port : null
135
+ const activeSockets = server._epActiveSockets?.size || 0
136
+ const webSockets = server._epWSServer?.clients?.size || 0
137
+ const lastUsedAt = server._epLastUsedAt || null
138
+
139
+ return {
140
+ host,
141
+ port,
142
+ activeSockets,
143
+ webSockets,
144
+ lastUsedAt,
145
+ idleForMs: lastUsedAt ? Date.now() - lastUsedAt : null,
146
+ }
147
+ })
148
+ const mitmTlsSockets = mitmServers.reduce((sum, item) => sum + item.activeSockets, 0)
149
+ const webSockets = (ctx.localWSServer?.clients?.size || 0)
150
+ + mitmServers.reduce((sum, item) => sum + item.webSockets, 0)
151
+ const connections = {
152
+ proxySockets: activeProxySockets.size,
153
+ mitmTlsSockets,
154
+ webSockets,
155
+ total: activeProxySockets.size + mitmTlsSockets + webSockets,
156
+ }
157
+
158
+ return {
159
+ connections,
160
+ mitmServers,
161
+ logRateLimit: rateLimitedLogger.getStats(),
162
+ }
163
+ },
164
+ config: parseWatchdogConfig(process.env),
165
+ })
166
+
167
+ function logRuntimeError(key, ...args) {
168
+ rateLimitedLogger.error(key, ...args)
169
+ }
170
+
171
+ function startRuntimeWatchdog() {
172
+ if (!runtimeHealth.config.enabled) return
173
+
174
+ const timer = setInterval(() => {
175
+ const evaluation = runtimeHealth.evaluate()
176
+ if (!evaluation.shouldWarn) return
177
+
178
+ const message = `watchdog ${evaluation.status}: ${evaluation.reasons.join('; ')} (${evaluation.consecutiveFailures}/${runtimeHealth.config.failureThreshold})`
179
+ rateLimitedLogger.warn('watchdog:runtime-health', message)
180
+ if (evaluation.shouldExit) {
181
+ handleFatalError('watchdog', new Error(message))
182
+ }
183
+ }, runtimeHealth.config.intervalMs)
184
+ timer.unref?.()
185
+ }
186
+
187
+ // ===== Express API 服务 =====
188
+ const { createApp } = require('./dist/server/index')
189
+
190
+ let configDiag = null
191
+
192
+ const serverContext = {
193
+ currentMocksPath: ctx.currentMocksPath,
194
+ routeRules: ctx.routeRules,
195
+ ruleMap: ctx.ruleMap,
196
+ excludeMap: ctx.excludeMap,
197
+ excludeMap: ctx.excludeMap,
198
+ proxyRecordArr: ctx.proxyRecordArr,
199
+ proxyRecordDetailMap: ctx.proxyRecordDetailMap,
200
+ recordIdSeq: ctx.recordIdSeq,
201
+ mockRules: ctx.mockRules,
202
+ mockIdSeq: ctx.mockIdSeq,
203
+ requestPipeline: ctx.requestPipeline,
204
+ builtinLoggerPlugin: ctx.builtinLoggerPlugin,
205
+ shadowCompareTracker: ctx.shadowCompareTracker,
206
+ onModeGate: ctx.onModeGate,
207
+ pluginManager: ctx.pluginManager,
208
+ hookDispatcher: ctx.hookDispatcher,
209
+ settingsPath: ctx.settingsPath,
210
+ meddleDir: ctx.meddleDir,
211
+ settings: null,
212
+ loadMockRules: () => {
213
+ mockHandler.loadMockRules()
214
+ serverContext.mockRules = ctx.mockRules
215
+ serverContext.mockIdSeq = ctx.mockIdSeq
216
+ },
217
+ saveMockRules: () => mockHandler.saveMockRules(),
218
+ reloadCustomPlugins: () => pluginBoot.reloadCustomPlugins(),
219
+ logRuleMap: () => routeLoader.logRuleMap(),
220
+ reloadAllRuleFiles: () => routeLoader.reloadAllRuleFiles(),
221
+ broadcastToAllClients: (data) => {
222
+ if (ctx.localWSServer) {
223
+ ctx.localWSServer.clients.forEach(client => {
224
+ if (client.readyState === 1) client.send(JSON.stringify(data))
225
+ })
226
+ }
227
+ },
228
+ appendProxyRecordFromPluginTest: (logData, detail) => {
229
+ const recordId = ctx.recordIdSeq++
230
+ const entry = { id: recordId, ...pluginClientIdentity(), ...logData }
231
+ appendProxyRecord(ctx, entry, detail)
232
+ },
233
+ getMockFilePath: () => mockHandler.getMockFilePath(),
234
+ performConfigDiagnostics: () => configDiag && configDiag.performConfigDiagnostics(),
235
+ loadSettingsSync: () => configDiag && configDiag.loadSettingsSync(),
236
+ refreshClientAliases: () => clientIdentityResolver.refresh(),
237
+ resolveTargetUrlForTest: (url) => resolveTargetUrl(url, ctx.routeRules) || url,
238
+ canUsePipelineExecuteForTest: (source) => pluginIntercept.canUsePipelineExecuteForSource(source),
239
+ matchMockRuleForTest: (url, method) => mockHandler.matchMockRule(url, method),
240
+ shouldUseMockForTest: (source, rule) => !pluginIntercept.shouldUsePluginMockForRequest(source, rule),
241
+ buildMockResponseForTest: (rule) => mockHandler.buildMockResponseForTest(rule),
242
+ getRemoteAccessInfo: () => {
243
+ const address = proxyServer.address()
244
+ const port = address && typeof address === 'object' ? address.port : null
245
+ return createRemoteAccessInfo(remoteAccess, lanAddresses, port)
246
+ },
247
+ getRuntimeHealth: () => runtimeHealth.snapshot(),
248
+ }
249
+
250
+ configDiag = createConfigDiagnostics(ctx, serverContext, mockHandler)
251
+
252
+ const routeLoader = createRouteLoader(ctx, serverContext)
253
+ const expressApp = createApp(serverContext)
254
+
255
+ // ===== 加载 Mock 和路由规则 =====
256
+ serverContext.loadMockRules()
257
+ routeLoader.initRouteRules()
258
+
259
+ // ===== Cross-Origin 插件 =====
260
+ const plugins = [{
261
+ name: 'Plugin:Cross-Origin',
262
+ beforeSendResponse(res) { res.setHeader('Access-Control-Allow-Origin', '*') }
263
+ }]
264
+
265
+ // ===== HTTP 代理服务器 =====
266
+ const proxyServer = http.createServer(async (req, res) => {
267
+ const serverPort = proxyServer.address().port
268
+ const requestUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`)
269
+ const requestTargetsProxy = isProxyHost(req.headers.host, serverPort, lanAddresses)
270
+
271
+ if (requestTargetsProxy) {
272
+ if (requestUrl.pathname === '/_meddle/ca.crt') {
273
+ const rootCAPath = getRootCAPath()
274
+ res.writeHead(200, {
275
+ 'Content-Type': 'application/x-x509-ca-cert',
276
+ 'Content-Disposition': 'attachment; filename="meddle-ca.crt"',
277
+ 'Cache-Control': 'no-store',
278
+ })
279
+ const certStream = fs.createReadStream(rootCAPath)
280
+ certStream.on('error', (err) => {
281
+ logRuntimeError('stream:ca-cert', 'CA certificate stream error:', getErrorMessage(err))
282
+ finishResponseWithProxyError(res, 500)
283
+ })
284
+ certStream.pipe(res)
285
+ return
286
+ }
287
+
288
+ if (remoteAccess.enabled && requestUrl.pathname === '/_meddle/setup') {
289
+ const requestedHost = requestUrl.hostname.replace(/^\[|\]$/g, '')
290
+ const setupHost = requestedHost === 'localhost' || isLoopbackAddress(requestedHost)
291
+ ? (lanAddresses[0] || requestedHost)
292
+ : requestedHost
293
+ const html = buildRemoteSetupHtml(
294
+ setupHost,
295
+ serverPort,
296
+ remoteAccess.interceptHttps,
297
+ !!remoteAccess.token,
298
+ )
299
+ res.writeHead(200, {
300
+ 'Content-Type': 'text/html; charset=utf-8',
301
+ 'Cache-Control': 'no-store',
302
+ })
303
+ res.end(html)
304
+ return
305
+ }
306
+
307
+ if (isLoopbackAddress(req.socket.remoteAddress)) {
308
+ handleLocalRequest(req, res, { expressApp, serverContext, ctx })
309
+ return
310
+ }
311
+
312
+ if (remoteAccess.enabled && requestUrl.pathname === '/') {
313
+ const html = buildRemoteSetupHtml(
314
+ requestUrl.hostname,
315
+ serverPort,
316
+ remoteAccess.interceptHttps,
317
+ !!remoteAccess.token,
318
+ )
319
+ res.writeHead(200, {
320
+ 'Content-Type': 'text/html; charset=utf-8',
321
+ 'Cache-Control': 'no-store',
322
+ })
323
+ res.end(html)
324
+ return
325
+ }
326
+
327
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
328
+ res.end('Remote access to the Meddle management interface is forbidden')
329
+ return
330
+ }
331
+
332
+ const access = authorizeProxyClient(req.socket.remoteAddress, req.headers, remoteAccess)
333
+ if (!access.allowed) {
334
+ const headers = { 'Content-Type': 'text/plain; charset=utf-8' }
335
+ if (access.statusCode === 407) headers['Proxy-Authenticate'] = 'Basic realm="Meddle"'
336
+ res.writeHead(access.statusCode, headers)
337
+ res.end(access.message)
338
+ return
339
+ }
340
+
341
+ const baseClientIdentity = clientIdentityResolver.resolve(req.socket.remoteAddress)
342
+ const clientIdentity = await enrichClientIdentityWithApplication(
343
+ baseClientIdentity,
344
+ applicationIdentityResolver,
345
+ { socket: req.socket, headers: req.headers },
346
+ )
347
+ req._epClientIdentity = clientIdentity
348
+ req.socket._epClientIdentity = clientIdentity
349
+
350
+ // ===== HTTP 代理 =====
351
+ const source = req.url
352
+
353
+ const mockRule = mockHandler.matchMockRule(source, req.method)
354
+ if (mockRule && !pluginIntercept.shouldUsePluginMockForRequest(source, mockRule)) {
355
+ return mockHandler.sendMockResponse(req, res, mockRule, { method: req.method, source, target: source })
356
+ }
357
+
358
+ const reqChunks = []
359
+ req.on('data', chunk => reqChunks.push(chunk))
360
+ req.on('error', (err) => {
361
+ logRuntimeError(`http:client-request:${getErrorKey(err)}`, 'HTTP client request error:', getErrorMessage(err))
362
+ finishResponseWithProxyError(res, 400)
363
+ })
364
+ req.on('end', async () => {
365
+ try {
366
+ const reqBody = Buffer.concat(reqChunks)
367
+ const legacyResolvedTarget = resolveTargetUrl(source, ctx.routeRules)
368
+ if (legacyResolvedTarget && legacyResolvedTarget.startsWith('file://')) {
369
+ return handleMapLocalRequest(ctx, req, res, source, legacyResolvedTarget)
370
+ }
371
+ const legacyTarget = legacyResolvedTarget || source
372
+ const routeDecision = await decideRoute({
373
+ source, method: req.method, headers: req.headers, reqBody,
374
+ legacyTarget, requestPipeline: ctx.requestPipeline,
375
+ canUsePipelineExecuteForSource: pluginIntercept.canUsePipelineExecuteForSource,
376
+ observeShadowDecision: pluginIntercept.observeShadowDecision,
377
+ fallbackResolve: async () => ({ target: legacyTarget, shortCircuited: false, response: null }),
378
+ })
379
+ let target = routeDecision.target
380
+ if (routeDecision.shortCircuited) { sendShortResponse(res, routeDecision.response); return }
381
+
382
+ const url = new URL(target.startsWith('http') ? target : req.url, 'http://' + req.headers.host)
383
+ const routeChanged = source !== url.href
384
+ const startTime = Date.now()
385
+ const intercepting = pluginIntercept.shouldInterceptResponse()
386
+
387
+ const proxyReq = http.request(url, { method: req.method, headers: stripProxyHeaders(req.headers) }, (proxyRes) => {
388
+ const resChunks = []
389
+ let proxyResponseSettled = false
390
+ const handleProxyResponseError = (err) => {
391
+ if (proxyResponseSettled) return
392
+ proxyResponseSettled = true
393
+ pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
394
+ logRuntimeError(`http:proxy-response:${getErrorKey(err)}`, 'HTTP proxy response error:', getErrorMessage(err))
395
+ finishResponseWithProxyError(res)
396
+ }
397
+ if (routeChanged) proxyRes.headers['x-real-url'] = url.href
398
+ if (!intercepting) res.writeHead(proxyRes.statusCode, proxyRes.headers)
399
+ proxyRes.on('data', chunk => { resChunks.push(chunk); if (!intercepting) res.write(chunk) })
400
+ proxyRes.on('error', handleProxyResponseError)
401
+ proxyRes.on('aborted', () => handleProxyResponseError(new Error('HTTP proxy response aborted')))
402
+ proxyRes.on('close', () => {
403
+ if (!proxyResponseSettled) handleProxyResponseError(new Error('HTTP proxy response closed before end'))
404
+ })
405
+ proxyRes.on('end', async () => {
406
+ if (proxyResponseSettled) return
407
+ proxyResponseSettled = true
408
+ try {
409
+ const resBody = Buffer.concat(resChunks)
410
+ let intercepted = false
411
+ if (intercepting) {
412
+ try {
413
+ intercepted = await pluginIntercept.interceptResponseWithPlugins({
414
+ req, res, source, target: url.href, startTime,
415
+ statusCode: proxyRes.statusCode, headers: proxyRes.headers,
416
+ bodyBuffer: resBody, reqBody, inspectionMeta: routeDecision.meta,
417
+ })
418
+ } catch (e) { console.error('[plugin] intercept error:', e) }
419
+ if (!intercepted) { res.writeHead(proxyRes.statusCode, proxyRes.headers); res.end(resBody) }
420
+ } else { res.end() }
421
+
422
+ const recordId = ctx.recordIdSeq++
423
+ const logData = {
424
+ id: recordId, method: req.method, source, target: url.href,
425
+ time: new Date().toLocaleTimeString(), statusCode: proxyRes.statusCode,
426
+ duration: Date.now() - startTime,
427
+ ...clientIdentity,
428
+ }
429
+ if (!intercepting) pluginIntercept.emitLegacyResponseToPlugins(logData)
430
+ const responseEncoding = proxyRes.headers && proxyRes.headers['content-encoding']
431
+ // 获取 inspection 信息
432
+ const inspectionStages = routeDecision.meta?._inspectionStages || []
433
+ const detail = {
434
+ requestHeaders: req.headers,
435
+ requestBody: safeBodyToString(reqBody, ctx.MAX_BODY_SIZE),
436
+ responseHeaders: proxyRes.headers,
437
+ responseBody: safeBodyToString(resBody, ctx.MAX_BODY_SIZE, responseEncoding),
438
+ statusCode: proxyRes.statusCode, statusMessage: proxyRes.statusMessage,
439
+ method: req.method, url: source,
440
+ inspection: inspectionStages.length > 0 ? {
441
+ url: source,
442
+ method: req.method,
443
+ stages: inspectionStages,
444
+ totalDuration: inspectionStages.reduce(function(sum, s) { return sum + s.duration }, 0),
445
+ } : undefined,
446
+ }
447
+ appendProxyRecord(ctx, logData, detail)
448
+ } catch (err) {
449
+ pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
450
+ logRuntimeError(`http:response-handler:${getErrorKey(err)}`, 'HTTP proxy response handling error:', getErrorMessage(err))
451
+ finishResponseWithProxyError(res)
452
+ }
453
+ })
454
+ })
455
+ proxyReq.setTimeout(UPSTREAM_REQUEST_TIMEOUT_MS, () => {
456
+ proxyReq.destroy(new Error('HTTP upstream request timeout'))
457
+ })
458
+ proxyReq.on('error', (err) => {
459
+ pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
460
+ logRuntimeError(`http:proxy-request:${getErrorKey(err)}`, 'HTTP proxy error:', getErrorMessage(err))
461
+ finishResponseWithProxyError(res)
462
+ })
463
+ proxyReq.write(reqBody)
464
+ proxyReq.end()
465
+ } catch (err) {
466
+ pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
467
+ logRuntimeError(`http:request-handler:${getErrorKey(err)}`, 'HTTP proxy request failed:', getErrorMessage(err))
468
+ finishResponseWithProxyError(res)
469
+ }
470
+ })
471
+ })
472
+
473
+ proxyServer.on('connection', (socket) => {
474
+ activeProxySockets.add(socket)
475
+ socket.once('close', () => { activeProxySockets.delete(socket) })
476
+ })
477
+
478
+ // ===== WebSocket 服务(日志推送)=====
479
+ // 必须使用 noServer,避免与下方 proxyServer.on('upgrade') 重复 handleUpgrade
480
+ const localWSServer = new WebSocketServer({ noServer: true })
481
+ ctx.localWSServer = localWSServer
482
+
483
+ localWSServer.on('connection', (client, req) => {})
484
+
485
+ // ===== 启动 =====
486
+ ;(async () => {
487
+ await ensureRootCA()
488
+ await pluginBoot.bootstrapBuiltinPlugins()
489
+ const port = await getFreePort()
490
+ proxyServer.listen(port, remoteAccess.bindHost, () => {
491
+ const proxyUrl = `http://127.0.0.1:${port}`
492
+ proxyDebug('proxy-server start on ' + chalk.green(proxyUrl))
493
+ proxyDebug('plugin pipeline mode: ' + chalk.cyan(ctx.requestPipeline.mode))
494
+ if (ctx.requestPipeline.mode === 'on') {
495
+ proxyDebug('plugin on host allowlist: ' + (ctx.PLUGIN_ON_HOSTS.size > 0 ? Array.from(ctx.PLUGIN_ON_HOSTS).join(',') : '(all)'))
496
+ }
497
+ if (process.env.MEDDLE_MCP) {
498
+ const mcpFile = path.join(ctx.meddleDir, 'mcp-proxy-url.json')
499
+ const mcpData = { proxyUrl }
500
+ if (process.env.MEDDLE_OPEN_CHROMEDEVTOOLS) mcpData.remoteDebuggingPort = 9222
501
+ fs.writeFileSync(mcpFile, JSON.stringify(mcpData), 'utf8')
502
+ }
503
+ if (ctx.AUTO_OPEN) {
504
+ const proxyAddr = `127.0.0.1:${port}`
505
+ const remotePort = process.env.MEDDLE_OPEN_CHROMEDEVTOOLS ? 9222 : undefined
506
+ if (openBrowserWithProxy(proxyUrl, proxyAddr, ctx.meddleDir, remotePort)) {
507
+ console.log(chalk.green('已启动浏览器(代理:'), proxyAddr + chalk.green(')'))
508
+ } else {
509
+ console.log(chalk.yellow('浏览器并未启动,请手动打开'), chalk.cyan(proxyUrl), chalk.yellow('并设置代理'), proxyAddr)
510
+ }
511
+ }
512
+ if (remoteAccess.enabled) {
513
+ console.log(chalk.green('远程代理已启用,HTTPS 解密:'), remoteAccess.interceptHttps ? '开启' : '关闭')
514
+ if (lanAddresses.length === 0) {
515
+ console.log(chalk.yellow('未发现可用的局域网 IPv4 地址'))
516
+ }
517
+ for (const address of lanAddresses) {
518
+ console.log(chalk.cyan(`手机配置入口: http://${address}:${port}/`))
519
+ console.log(chalk.cyan(`代理服务器: ${address}:${port}`))
520
+ }
521
+ if (remoteAccess.token) {
522
+ console.log(chalk.green('代理认证已启用,用户名: meddle'))
523
+ } else {
524
+ console.log(chalk.yellow('代理认证未启用,请仅在可信局域网中使用'))
525
+ }
526
+ }
527
+ })
528
+ })()
529
+
530
+ // ===== HTTPS CONNECT 处理 =====
531
+ proxyServer.on('connect', async (req, socket, head) => {
532
+ socket.on('error', (err) => {
533
+ if (!isExpectedSocketError(err)) proxyDebug('client CONNECT socket error', req.url, err.message)
534
+ })
535
+
536
+ const access = authorizeProxyClient(socket.remoteAddress, req.headers, remoteAccess)
537
+ if (!access.allowed) {
538
+ const authenticate = access.statusCode === 407
539
+ ? 'Proxy-Authenticate: Basic realm="Meddle"\r\n'
540
+ : ''
541
+ socket.end(`HTTP/1.1 ${access.statusCode} ${access.message}\r\n${authenticate}Connection: close\r\n\r\n`)
542
+ return
543
+ }
544
+
545
+ const authority = parseConnectAuthority(req.url)
546
+ if (!authority) {
547
+ socket.end('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n')
548
+ return
549
+ }
550
+ const originHost = authority.host
551
+ const baseClientIdentity = clientIdentityResolver.resolve(socket.remoteAddress)
552
+ const clientIdentity = await enrichClientIdentityWithApplication(
553
+ baseClientIdentity,
554
+ applicationIdentityResolver,
555
+ { socket },
556
+ )
557
+ const needDecrypt = remoteAccess.interceptHttps
558
+ || !!resolveTargetUrl(`https://${req.url}/`, ctx.routeRules)
559
+ proxyDebug('received connect request....', needDecrypt ? '(decrypt)' : '(tunnel)')
560
+
561
+ socket.on('end', () => {})
562
+
563
+ // 无规则:直接隧道转发
564
+ if (!needDecrypt) {
565
+ const { host, port } = authority
566
+ const connection = connect({ host, port }, () => {
567
+ establishConnectTunnel(socket, connection, head)
568
+ })
569
+ connection.on('error', (err) => {
570
+ if (!isExpectedSocketError(err)) {
571
+ proxyDebug('tunnel connect error', host + ':' + port, err.message)
572
+ }
573
+ if (!socket.destroyed) {
574
+ try { socket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n') } catch (_) {}
575
+ }
576
+ })
577
+ return
578
+ }
579
+
580
+ function createHttpsServerByCert() {
581
+ return new Promise((resolve, reject) => {
582
+ crtMgr.getCertificate(originHost, (error, key, crt) => {
583
+ if (error) return reject(error)
584
+ const clientIdentityRegistry = createMitmClientIdentityRegistry()
585
+ const server = http2.createSecureServer({ cert: crt, key, allowHTTP1: true }, async (req, res) => {
586
+ const inheritedClientIdentity = getRequestClientIdentity(req)
587
+ const requestClientIdentity = await enrichClientIdentityWithApplication(
588
+ inheritedClientIdentity,
589
+ applicationIdentityResolver,
590
+ { headers: req.headers },
591
+ )
592
+ req._epClientIdentity = requestClientIdentity
593
+ req.socket._epClientIdentity = requestClientIdentity
594
+ const source = 'https://' + (req.headers.host || req.authority || originHost) + req.url
595
+
596
+ const mockRule = mockHandler.matchMockRule(source, req.method)
597
+ if (mockRule && !pluginIntercept.shouldUsePluginMockForRequest(source, mockRule)) {
598
+ return mockHandler.sendMockResponse(req, res, mockRule, { method: req.method, source, target: source })
599
+ }
600
+
601
+ const reqChunks = []
602
+ req.on('data', chunk => reqChunks.push(chunk))
603
+ req.on('error', (err) => {
604
+ logRuntimeError(`https:client-request:${getErrorKey(err)}`, 'HTTPS client request error:', getErrorMessage(err))
605
+ finishResponseWithProxyError(res, 400)
606
+ })
607
+ req.on('end', async () => {
608
+ try {
609
+ const reqBody = Buffer.concat(reqChunks)
610
+ const legacyResolvedTarget = resolveTargetUrl(source, ctx.routeRules)
611
+ if (legacyResolvedTarget && legacyResolvedTarget.startsWith('file://')) {
612
+ return handleMapLocalRequest(ctx, req, res, source, legacyResolvedTarget)
613
+ }
614
+ const legacyTarget = legacyResolvedTarget || source
615
+ const routeDecision = await decideRoute({
616
+ source, method: req.method, headers: req.headers, reqBody,
617
+ legacyTarget, requestPipeline: ctx.requestPipeline,
618
+ canUsePipelineExecuteForSource: pluginIntercept.canUsePipelineExecuteForSource,
619
+ observeShadowDecision: pluginIntercept.observeShadowDecision,
620
+ fallbackResolve: async () => ({ target: legacyTarget, shortCircuited: false, response: null }),
621
+ })
622
+ let target = routeDecision.target
623
+ if (routeDecision.shortCircuited) { sendShortResponse(res, routeDecision.response); return }
624
+
625
+ const routeChanged = source !== target
626
+ const startTime = Date.now()
627
+ const intercepting = pluginIntercept.shouldInterceptResponse()
628
+ try {
629
+ const proxyRes = await makeProxyRequest(target, req.method, stripProxyHeaders(req.headers), reqBody)
630
+ const resChunks = []
631
+ let proxyResponseSettled = false
632
+ const handleProxyResponseError = (err) => {
633
+ if (proxyResponseSettled) return
634
+ proxyResponseSettled = true
635
+ pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
636
+ logRuntimeError(`https:upstream-stream:${originHost}:${getErrorKey(err)}`, '[proxy] upstream stream %s: %s', originHost + req.url, getErrorMessage(err))
637
+ finishResponseWithProxyError(res)
638
+ }
639
+ if (routeChanged) proxyRes.headers['x-real-url'] = target
640
+ if (!intercepting) res.writeHead(proxyRes.statusCode, cleanHeadersForH2(proxyRes.headers))
641
+
642
+ proxyRes.stream.on('data', chunk => { resChunks.push(chunk); if (!intercepting) res.write(chunk) })
643
+ proxyRes.stream.on('error', handleProxyResponseError)
644
+ proxyRes.stream.on('aborted', () => handleProxyResponseError(new Error('upstream stream aborted')))
645
+ proxyRes.stream.on('close', () => {
646
+ if (!proxyResponseSettled) handleProxyResponseError(new Error('upstream stream closed before end'))
647
+ })
648
+ proxyRes.stream.on('end', async () => {
649
+ if (proxyResponseSettled) return
650
+ proxyResponseSettled = true
651
+ try {
652
+ const resBody = Buffer.concat(resChunks)
653
+ let intercepted = false
654
+ if (intercepting) {
655
+ try {
656
+ intercepted = await pluginIntercept.interceptResponseWithPlugins({
657
+ req, res, source, target, startTime,
658
+ statusCode: proxyRes.statusCode, headers: proxyRes.headers,
659
+ bodyBuffer: resBody, reqBody, inspectionMeta: routeDecision.meta, cleanHeaders: cleanHeadersForH2,
660
+ })
661
+ } catch (e) { console.error('[plugin] intercept error (HTTPS):', e) }
662
+ if (!intercepted) { res.writeHead(proxyRes.statusCode, cleanHeadersForH2(proxyRes.headers)); res.end(resBody) }
663
+ } else { res.end() }
664
+
665
+ const recordId = ctx.recordIdSeq++
666
+ const logData = {
667
+ id: recordId, method: req.method, source, target,
668
+ time: new Date().toLocaleTimeString(), protocol: proxyRes.protocol,
669
+ statusCode: proxyRes.statusCode, duration: Date.now() - startTime,
670
+ ...requestClientIdentity,
671
+ }
672
+ if (!intercepting) pluginIntercept.emitLegacyResponseToPlugins(logData)
673
+ const responseEncoding = proxyRes.headers && proxyRes.headers['content-encoding']
674
+ // 获取 inspection 信息
675
+ const inspectionStages = routeDecision.meta?._inspectionStages || []
676
+ const detail = {
677
+ requestHeaders: req.headers,
678
+ requestBody: safeBodyToString(reqBody, ctx.MAX_BODY_SIZE),
679
+ responseHeaders: proxyRes.headers,
680
+ responseBody: safeBodyToString(resBody, ctx.MAX_BODY_SIZE, responseEncoding),
681
+ statusCode: proxyRes.statusCode, statusMessage: proxyRes.statusMessage,
682
+ method: req.method, url: source,
683
+ inspection: inspectionStages.length > 0 ? {
684
+ url: source,
685
+ method: req.method,
686
+ stages: inspectionStages,
687
+ totalDuration: inspectionStages.reduce(function(sum, s) { return sum + s.duration }, 0),
688
+ } : undefined,
689
+ }
690
+ appendProxyRecord(ctx, logData, detail)
691
+ } catch (err) {
692
+ pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
693
+ logRuntimeError(`https:response-handler:${originHost}:${getErrorKey(err)}`, '[proxy] upstream response handling %s: %s', originHost + req.url, getErrorMessage(err))
694
+ finishResponseWithProxyError(res)
695
+ }
696
+ })
697
+ } catch (err) {
698
+ const code = err.code || ''
699
+ const isConnReset = code === 'ECONNRESET' || code === 'ETIMEDOUT' || code === 'ECONNREFUSED'
700
+ if (isConnReset) {
701
+ logRuntimeError(`https:upstream:${originHost}:${code}`, '[proxy] upstream %s %s: %s', originHost + req.url, code, err.message)
702
+ } else {
703
+ logRuntimeError(`https:upstream:${originHost}:${getErrorKey(err)}`, '[error debug]', originHost + req.url, err)
704
+ }
705
+ pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
706
+ finishResponseWithProxyError(res)
707
+ }
708
+ } catch (err) {
709
+ pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
710
+ logRuntimeError(`https:request-handler:${originHost}:${getErrorKey(err)}`, '[proxy] HTTPS request handling %s: %s', originHost + req.url, getErrorMessage(err))
711
+ finishResponseWithProxyError(res)
712
+ }
713
+ })
714
+ })
715
+ markMitmServerUsed(server)
716
+ server._epActiveSockets = new Set()
717
+
718
+ server._epRegisterClientIdentity = (remotePort, identity) => {
719
+ clientIdentityRegistry.register(remotePort, identity)
720
+ }
721
+ server.on('secureConnection', tlsSocket => {
722
+ markMitmServerUsed(server)
723
+ server._epActiveSockets.add(tlsSocket)
724
+ tlsSocket.once('close', () => {
725
+ server._epActiveSockets.delete(tlsSocket)
726
+ markMitmServerUsed(server)
727
+ })
728
+ clientIdentityRegistry.attach(tlsSocket)
729
+ })
730
+
731
+ // WebSocket 代理(MITM HTTPS 服务器)- 使用 noServer 模式,统一在此处理并强制上游→客户端为文本
732
+ const wss = new WebSocketServer({ noServer: true })
733
+ server._epWSServer = wss
734
+ server.on('upgrade', (req, socket, head) => {
735
+ markMitmServerUsed(server)
736
+ socket.on('error', (err) => {
737
+ if (err.code !== 'ECONNRESET') proxyDebug('[ws] mitm upgrade socket error:', err.message)
738
+ })
739
+ if (socket._wsUpgradeHandled) return
740
+ socket._wsUpgradeHandled = true
741
+ wss.handleUpgrade(req, socket, head, (ws) => {
742
+ const source = 'wss://' + (req.headers.host || originHost) + req.url
743
+ let targetUrl = resolveTargetUrl(source, ctx.routeRules)
744
+ if (!targetUrl) targetUrl = source
745
+
746
+ const outHeaders = { ...req.headers }
747
+ try {
748
+ const u = new URL(targetUrl)
749
+ outHeaders.host = u.host
750
+ if (!outHeaders.origin) outHeaders.origin = u.origin
751
+ } catch (_) {}
752
+
753
+ const proxyWs = new WebSocket(targetUrl, ws.protocol || [], {
754
+ rejectUnauthorized: false,
755
+ headers: outHeaders
756
+ })
757
+ const OPEN = 1
758
+ const CONNECTING = 0
759
+ let closed = false
760
+ const clientBuffer = []
761
+ const safeClose = (sock, code, reason) => {
762
+ try {
763
+ if (sock.readyState === OPEN || sock.readyState === CONNECTING) sock.close(code, reason)
764
+ } catch (_) {}
765
+ }
766
+ const closePeer = (sock, code, reason) => {
767
+ if (closed) return
768
+ closed = true
769
+ clientBuffer.length = 0
770
+ safeClose(sock, code, reason)
771
+ }
772
+ const safeSend = (sock, data, label, isBinary) => {
773
+ if (sock.readyState !== OPEN) return
774
+ try {
775
+ const cb = (err) => {
776
+ if (err && err.message !== 'WebSocket is not open') proxyDebug(`[ws] ${label} send error: ${err.message}`)
777
+ }
778
+ if (typeof isBinary === 'boolean') {
779
+ sock.send(data, { binary: isBinary }, cb)
780
+ } else {
781
+ sock.send(data, cb)
782
+ }
783
+ } catch (e) {
784
+ if (e.message !== 'WebSocket is not open') proxyDebug(`[ws] ${label} send: ${e.message}`)
785
+ }
786
+ }
787
+ const flushClientBuffer = () => {
788
+ while (clientBuffer.length) {
789
+ const item = clientBuffer.shift()
790
+ safeSend(proxyWs, item.data, 'upstream', item.isBinary)
791
+ }
792
+ }
793
+ ws.on('message', (data, isBinary) => {
794
+ const type = isBinary ? 'binary' : (typeof data === 'string' ? 'text' : 'unknown')
795
+ let preview = ''
796
+ if (typeof data === 'string') {
797
+ try { preview = JSON.parse(data) ? '(valid json)' : '(text)' } catch { preview = '(text)' }
798
+ } else if (Buffer.isBuffer(data)) {
799
+ preview = `(buffer ${data.length} bytes)`
800
+ }
801
+ proxyDebug(`[ws] client -> upstream: ${type} ${preview}`)
802
+ if (proxyWs.readyState === OPEN) {
803
+ safeSend(proxyWs, data, 'upstream', isBinary)
804
+ } else if (proxyWs.readyState === CONNECTING) {
805
+ if (clientBuffer.length >= MAX_WS_BUFFERED_MESSAGES) {
806
+ closePeer(ws, 1011, 'upstream not ready')
807
+ safeClose(proxyWs, 1011, 'upstream not ready')
808
+ return
809
+ }
810
+ clientBuffer.push({ data, isBinary })
811
+ }
812
+ })
813
+ proxyWs.on('open', () => {
814
+ flushClientBuffer()
815
+ proxyWs.on('message', (data, isBinary) => {
816
+ const type = isBinary ? 'binary' : (typeof data === 'string' ? 'text' : 'unknown')
817
+ let preview = ''
818
+ if (typeof data === 'string') {
819
+ try { preview = JSON.parse(data) ? '(valid json)' : '(text)' } catch { preview = '(text)' }
820
+ } else if (Buffer.isBuffer(data)) {
821
+ preview = `(buffer ${data.length} bytes)`
822
+ }
823
+ proxyDebug(`[ws] upstream -> client: ${type} ${preview}`)
824
+ safeSend(ws, data, 'client', isBinary)
825
+ })
826
+ })
827
+ proxyWs.on('error', (e) => {
828
+ proxyDebug(`[ws] upstream ${targetUrl} error: ${e.message}`)
829
+ closePeer(ws, 1011, 'upstream error')
830
+ })
831
+ proxyWs.on('close', (code, reason) => {
832
+ closePeer(ws, code, reason)
833
+ })
834
+ ws.on('error', (e) => { proxyDebug(`[ws] client error: ${e.message}`) })
835
+ ws.on('close', (code, reason) => {
836
+ _debug('log')('ws close', code, reason)
837
+ closePeer(proxyWs, code, reason)
838
+ })
839
+ })
840
+ })
841
+
842
+ resolve(server)
843
+ })
844
+ })
845
+ }
846
+
847
+ try {
848
+ let server = ctx.httpsServerMap.get(originHost)
849
+ if (!server) {
850
+ const port = await getFreePort()
851
+ server = await createHttpsServerByCert()
852
+ server.listen(port, '127.0.0.1', () => { proxyDebug('listening on ' + port) })
853
+ ctx.httpsServerMap.set(originHost, server)
854
+ } else {
855
+ markMitmServerUsed(server)
856
+ }
857
+
858
+ if (server.listening) {
859
+ connectToLocalHttpsServer(server)
860
+ } else {
861
+ server.once('listening', () => { connectToLocalHttpsServer(server) })
862
+ }
863
+ } catch (err) {
864
+ proxyDebug('MITM setup error', originHost, err.message)
865
+ if (!socket.destroyed) {
866
+ try { socket.end('HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n') } catch (_) {}
867
+ }
868
+ }
869
+
870
+ function connectToLocalHttpsServer(server) {
871
+ markMitmServerUsed(server)
872
+ const connection = connect({
873
+ host: '127.0.0.1',
874
+ port: server.address().port,
875
+ }, () => {
876
+ server._epRegisterClientIdentity?.(connection.localPort, clientIdentity)
877
+ establishConnectTunnel(socket, connection, head)
878
+ })
879
+ connection.on('error', (err) => {
880
+ if (!isExpectedSocketError(err)) {
881
+ proxyDebug('local MITM connection error', originHost, err.message)
882
+ }
883
+ if (!socket.destroyed) socket.destroy()
884
+ })
885
+ }
886
+ })
887
+
888
+ proxyServer.on('upgrade', (req, socket, head) => {
889
+ socket.on('error', (err) => {
890
+ if (err.code !== 'ECONNRESET') proxyDebug('[ws] upgrade socket error:', err.message)
891
+ })
892
+
893
+ if (isLoopbackAddress(socket.remoteAddress) && (req.url === '/ws' || req.url.startsWith('/ws?'))) {
894
+ if (socket._epWsUpgradeHandled) return
895
+ socket._epWsUpgradeHandled = true
896
+ localWSServer.handleUpgrade(req, socket, head, (ws) => {
897
+ localWSServer.emit('connection', ws, req)
898
+ })
899
+ return
900
+ }
901
+
902
+ socket.destroy()
903
+ })
904
+
905
+ let fatalErrorInProgress = false
906
+ const MAX_FATAL_LOG_CHARS = 20000
907
+
908
+ function formatFatalError(reason) {
909
+ if (reason && reason.stack) return String(reason.stack)
910
+ if (reason instanceof Error) return `${reason.name}: ${reason.message}`
911
+ if (typeof reason === 'string') return reason
912
+
913
+ try {
914
+ return JSON.stringify(reason)
915
+ } catch (_) {
916
+ return String(reason)
917
+ }
918
+ }
919
+
920
+ function writeFatalError(type, reason) {
921
+ let message = `[fatal] ${type}\n${formatFatalError(reason)}\n`
922
+ if (message.length > MAX_FATAL_LOG_CHARS) {
923
+ message = message.slice(0, MAX_FATAL_LOG_CHARS) + '\n[fatal] stack truncated\n'
924
+ }
925
+
926
+ try {
927
+ fs.writeSync(2, message)
928
+ } catch (_) {
929
+ // If stderr itself fails, exit anyway; the process is in an undefined state.
930
+ }
931
+ }
932
+
933
+ function handleFatalError(type, reason) {
934
+ if (!fatalErrorInProgress) {
935
+ fatalErrorInProgress = true
936
+ writeFatalError(type, reason)
937
+ }
938
+
939
+ process.exit(1)
940
+ }
941
+
942
+ process.on('uncaughtException', (err) => {
943
+ handleFatalError('uncaughtException', err)
944
+ })
945
+
946
+ process.on('unhandledRejection', (reason) => {
947
+ handleFatalError('unhandledRejection', reason)
948
+ })
949
+
950
+ startRuntimeWatchdog()