@mswjs/interceptors 0.41.8 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +274 -228
- package/lib/browser/{bufferUtils-BiiO6HZv.mjs → buffer-utils-DJj7YzLG.js} +3 -3
- package/lib/browser/buffer-utils-DJj7YzLG.js.map +1 -0
- package/lib/browser/create-request-id-DlEd4GOA.js +184 -0
- package/lib/browser/create-request-id-DlEd4GOA.js.map +1 -0
- package/lib/browser/{getRawRequest-B1BqgWG6.mjs → fetch-utils-zxA_SD66.js} +123 -52
- package/lib/browser/fetch-utils-zxA_SD66.js.map +1 -0
- package/lib/browser/handle-request-CIOa9O-N.js +167 -0
- package/lib/browser/handle-request-CIOa9O-N.js.map +1 -0
- package/lib/{node/hasConfigurableGlobal-BiTmog1u.mjs → browser/has-configurable-global-Cew-dYqk.js} +5 -7
- package/lib/browser/has-configurable-global-Cew-dYqk.js.map +1 -0
- package/lib/browser/http-CB9rQ7i_.d.ts +105 -0
- package/lib/browser/{index.d.cts → index.d.ts} +33 -38
- package/lib/browser/index.js +130 -0
- package/lib/browser/index.js.map +1 -0
- package/lib/browser/interceptor-dtJZ_9QT.d.ts +50 -0
- package/lib/browser/interceptors/WebSocket/index.d.ts +2 -0
- package/lib/browser/interceptors/WebSocket/{index.mjs → index.js} +40 -31
- package/lib/browser/interceptors/WebSocket/index.js.map +1 -0
- package/lib/browser/interceptors/XMLHttpRequest/web.d.ts +11 -0
- package/lib/browser/interceptors/XMLHttpRequest/web.js +2 -0
- package/lib/browser/interceptors/fetch/web.d.ts +14 -0
- package/lib/browser/interceptors/fetch/web.js +2 -0
- package/lib/browser/presets/browser.d.ts +11 -0
- package/lib/browser/presets/browser.js +12 -0
- package/lib/browser/presets/browser.js.map +1 -0
- package/lib/browser/{resolveWebSocketUrl-C83-x9iE.mjs → resolve-web-socket-url-CSvNPLGi.js} +3 -3
- package/lib/browser/resolve-web-socket-url-CSvNPLGi.js.map +1 -0
- package/lib/browser/web-BjVDkiX0.js +248 -0
- package/lib/browser/web-BjVDkiX0.js.map +1 -0
- package/lib/{node/XMLHttpRequest-DhupxLHX.mjs → browser/web-C2oKsT3Q.js} +328 -256
- package/lib/browser/web-C2oKsT3Q.js.map +1 -0
- package/lib/browser/{interceptors/WebSocket/index.d.cts → websocket-CC0nB0md.d.ts} +41 -33
- package/lib/node/batch-interceptor-ByEZVih3.js +119 -0
- package/lib/node/batch-interceptor-ByEZVih3.js.map +1 -0
- package/lib/node/batch-interceptor-CPjFK2td.d.ts +28 -0
- package/lib/node/buffer-utils-BvPY1Tc-.js +187 -0
- package/lib/node/buffer-utils-BvPY1Tc-.js.map +1 -0
- package/lib/node/fetch-utils-Dw1PsmtX.js +568 -0
- package/lib/node/fetch-utils-Dw1PsmtX.js.map +1 -0
- package/lib/node/has-configurable-global-CJCh_g1I.js +22 -0
- package/lib/node/has-configurable-global-CJCh_g1I.js.map +1 -0
- package/lib/node/http-CB9rQ7i_.d.ts +105 -0
- package/lib/{browser/interceptors/WebSocket/index.d.mts → node/index.d.ts} +120 -60
- package/lib/node/{index.mjs → index.js} +8 -11
- package/lib/node/index.js.map +1 -0
- package/lib/node/interceptor-dtJZ_9QT.d.ts +50 -0
- package/lib/node/interceptors/ClientRequest/index.d.ts +14 -0
- package/lib/node/interceptors/ClientRequest/index.js +67 -0
- package/lib/node/interceptors/ClientRequest/index.js.map +1 -0
- package/lib/node/interceptors/XMLHttpRequest/node.d.ts +12 -0
- package/lib/node/interceptors/XMLHttpRequest/node.js +74 -0
- package/lib/node/interceptors/XMLHttpRequest/node.js.map +1 -0
- package/lib/node/interceptors/fetch/node.d.ts +18 -0
- package/lib/node/interceptors/fetch/node.js +83 -0
- package/lib/node/interceptors/fetch/node.js.map +1 -0
- package/lib/node/interceptors/http/index.d.ts +14 -0
- package/lib/node/interceptors/http/index.js +32 -0
- package/lib/node/interceptors/http/index.js.map +1 -0
- package/lib/node/interceptors/net/index.d.ts +203 -0
- package/lib/node/interceptors/net/index.js +2 -0
- package/lib/node/llhttp/constants.cjs +485 -0
- package/lib/node/llhttp/constants.d.cts +957 -0
- package/lib/node/llhttp/llhttp.wasm +0 -0
- package/lib/node/net-DtMnyEeg.js +1388 -0
- package/lib/node/net-DtMnyEeg.js.map +1 -0
- package/lib/node/presets/node.d.ts +12 -0
- package/lib/node/presets/node.js +17 -0
- package/lib/node/presets/node.js.map +1 -0
- package/lib/node/{RemoteHttpInterceptor.d.cts → remote-http-interceptor.d.ts} +12 -11
- package/lib/node/{RemoteHttpInterceptor.mjs → remote-http-interceptor.js} +38 -33
- package/lib/node/remote-http-interceptor.js.map +1 -0
- package/lib/node/source-lP0yyEtA.js +1990 -0
- package/lib/node/source-lP0yyEtA.js.map +1 -0
- package/package.json +79 -118
- package/src/batch-interceptor.test.ts +475 -0
- package/src/batch-interceptor.ts +244 -0
- package/src/{createRequestId.test.ts → create-request-id.test.ts} +1 -2
- package/src/disposable.ts +13 -0
- package/src/events/http.ts +92 -0
- package/src/events/websocket.ts +47 -0
- package/src/glossary.ts +1 -27
- package/src/index.ts +18 -10
- package/src/interceptor.test.ts +279 -0
- package/src/interceptor.ts +139 -0
- package/src/interceptors/ClientRequest/index.test.ts +21 -39
- package/src/interceptors/ClientRequest/index.ts +65 -180
- package/src/interceptors/ClientRequest/utils/{recordRawHeaders.test.ts → record-raw-headers.test.ts} +1 -2
- package/src/interceptors/ClientRequest/utils/{recordRawHeaders.ts → record-raw-headers.ts} +29 -2
- package/src/interceptors/WebSocket/index.ts +34 -51
- package/src/interceptors/WebSocket/utils/{bindEvent.test.ts → bind-event.test.ts} +1 -2
- package/src/interceptors/WebSocket/utils/events.test.ts +0 -1
- package/src/interceptors/WebSocket/{WebSocketClassTransport.ts → web-socket-class-transport.ts} +3 -3
- package/src/interceptors/WebSocket/{WebSocketClientConnection.ts → web-socket-client-connection.ts} +4 -4
- package/src/interceptors/WebSocket/{WebSocketOverride.ts → web-socket-override.ts} +6 -7
- package/src/interceptors/WebSocket/{WebSocketServerConnection.ts → web-socket-server-connection.ts} +4 -4
- package/src/interceptors/WebSocket/{WebSocketTransport.ts → web-socket-transport.ts} +1 -1
- package/src/interceptors/XMLHttpRequest/node.ts +104 -0
- package/src/interceptors/XMLHttpRequest/polyfills/{ProgressEventPolyfill.ts → progress-event-polyfill.ts} +1 -1
- package/src/interceptors/XMLHttpRequest/utils/{concateArrayBuffer.test.ts → concat-array-buffer.test.ts} +1 -2
- package/src/interceptors/XMLHttpRequest/utils/{concatArrayBuffer.ts → concat-array-buffer.ts} +1 -1
- package/src/interceptors/XMLHttpRequest/utils/{createEvent.test.ts → create-event.test.ts} +3 -4
- package/src/interceptors/XMLHttpRequest/utils/{createEvent.ts → create-event.ts} +2 -2
- package/src/interceptors/XMLHttpRequest/utils/{createResponse.ts → create-response.ts} +1 -1
- package/src/interceptors/XMLHttpRequest/utils/{getBodyByteLength.test.ts → get-body-byte-length.test.ts} +1 -2
- package/src/interceptors/XMLHttpRequest/web.ts +34 -0
- package/src/interceptors/XMLHttpRequest/{XMLHttpRequestController.ts → xml-http-request-controller.ts} +363 -204
- package/src/interceptors/XMLHttpRequest/{XMLHttpRequestProxy.ts → xml-http-request-proxy.ts} +28 -25
- package/src/interceptors/fetch/node.ts +103 -0
- package/src/interceptors/fetch/utils/{followRedirect.ts → follow-redirect.ts} +1 -1
- package/src/interceptors/fetch/web.ts +217 -0
- package/src/interceptors/http/forward-events.test.ts +86 -0
- package/src/interceptors/http/forward-events.ts +139 -0
- package/src/interceptors/http/http-parser/index.ts +310 -0
- package/src/interceptors/http/http-parser/llhttp/constants.cjs +485 -0
- package/src/interceptors/http/http-parser/llhttp/constants.d.cts +957 -0
- package/src/interceptors/http/http-parser/llhttp/llhttp.wasm +0 -0
- package/src/interceptors/http/http-parser.ts +138 -0
- package/src/interceptors/http/index.ts +34 -0
- package/src/interceptors/http/source.ts +688 -0
- package/src/interceptors/net/index.ts +368 -0
- package/src/interceptors/net/socket-controller.ts +1476 -0
- package/src/interceptors/net/utils/address-info.ts +54 -0
- package/src/interceptors/net/utils/connection-options-to-url.ts +56 -0
- package/src/interceptors/net/utils/flush-writes.ts +57 -0
- package/src/interceptors/net/utils/get-tls-connect-options.ts +23 -0
- package/src/interceptors/net/utils/normalize-net-connect-args.test.ts +233 -0
- package/src/interceptors/net/utils/normalize-net-connect-args.ts +121 -0
- package/src/interceptors/net/utils/normalize-tls-connect-args.ts +50 -0
- package/src/presets/browser.ts +4 -4
- package/src/presets/node.ts +4 -4
- package/src/{RemoteHttpInterceptor.ts → remote-http-interceptor.ts} +55 -42
- package/src/request-context.ts +40 -0
- package/src/{RequestController.test.ts → request-controller.test.ts} +37 -3
- package/src/{RequestController.ts → request-controller.ts} +33 -11
- package/src/utils/{bufferUtils.test.ts → buffer-utils.test.ts} +1 -2
- package/src/utils/buffer-utils.ts +37 -0
- package/src/utils/{cloneObject.test.ts → clone-object.test.ts} +1 -2
- package/src/utils/{cloneObject.ts → clone-object.ts} +8 -8
- package/src/utils/{createProxy.test.ts → create-proxy.test.ts} +6 -4
- package/src/utils/{createProxy.ts → create-proxy.ts} +1 -1
- package/src/utils/{fetchUtils.test.ts → fetch-utils.test.ts} +1 -1
- package/src/utils/{fetchUtils.ts → fetch-utils.ts} +28 -3
- package/src/utils/{findPropertySource.test.ts → find-property-source.test.ts} +1 -2
- package/src/utils/{getCleanUrl.test.ts → get-clean-url.test.ts} +1 -2
- package/src/utils/{getUrlByRequestOptions.test.ts → get-url-by-request-options.test.ts} +4 -8
- package/src/utils/{getUrlByRequestOptions.ts → get-url-by-request-options.ts} +16 -16
- package/src/utils/{getValueBySymbol.test.ts → get-value-by-symbol.test.ts} +1 -2
- package/src/utils/{handleRequest.ts → handle-request.ts} +66 -47
- package/src/utils/{hasConfigurableGlobal.test.ts → has-configurable-global.test.ts} +1 -2
- package/src/utils/{hasConfigurableGlobal.ts → has-configurable-global.ts} +2 -4
- package/src/utils/{isObject.test.ts → is-object.test.ts} +1 -2
- package/src/utils/logger.test.ts +154 -0
- package/src/utils/logger.ts +154 -0
- package/src/utils/{parseJson.test.ts → parse-json.test.ts} +1 -2
- package/src/utils/{patchesRegistry.test.ts → patches-registry.test.ts} +1 -1
- package/src/utils/{patchesRegistry.ts → patches-registry.ts} +1 -1
- package/src/utils/{responseUtils.ts → response-utils.ts} +25 -2
- package/ClientRequest/package.json +0 -11
- package/RemoteHttpInterceptor/package.json +0 -11
- package/WebSocket/package.json +0 -12
- package/XMLHttpRequest/package.json +0 -12
- package/fetch/package.json +0 -12
- package/lib/browser/Interceptor-Deczogc8.d.cts +0 -65
- package/lib/browser/Interceptor-gqKgs-aF.d.mts +0 -65
- package/lib/browser/XMLHttpRequest-B11I9TBx.cjs +0 -751
- package/lib/browser/XMLHttpRequest-B11I9TBx.cjs.map +0 -1
- package/lib/browser/XMLHttpRequest-DHEkSoRN.mjs +0 -746
- package/lib/browser/XMLHttpRequest-DHEkSoRN.mjs.map +0 -1
- package/lib/browser/bufferUtils-BiiO6HZv.mjs.map +0 -1
- package/lib/browser/bufferUtils-Uc0eRItL.cjs +0 -38
- package/lib/browser/bufferUtils-Uc0eRItL.cjs.map +0 -1
- package/lib/browser/createRequestId-DOf8Ktjs.cjs +0 -205
- package/lib/browser/createRequestId-DOf8Ktjs.cjs.map +0 -1
- package/lib/browser/createRequestId-DYCsFHOi.mjs +0 -170
- package/lib/browser/createRequestId-DYCsFHOi.mjs.map +0 -1
- package/lib/browser/fetch-BJwfdses.mjs +0 -236
- package/lib/browser/fetch-BJwfdses.mjs.map +0 -1
- package/lib/browser/fetch-M8iXkZ3L.cjs +0 -241
- package/lib/browser/fetch-M8iXkZ3L.cjs.map +0 -1
- package/lib/browser/getRawRequest-B1BqgWG6.mjs.map +0 -1
- package/lib/browser/getRawRequest-DdfaiPVH.cjs +0 -376
- package/lib/browser/getRawRequest-DdfaiPVH.cjs.map +0 -1
- package/lib/browser/glossary-1lhZx6Aw.d.cts +0 -69
- package/lib/browser/glossary-nkHWcwvY.d.mts +0 -69
- package/lib/browser/handleRequest-1Yqu_ho_.mjs +0 -178
- package/lib/browser/handleRequest-1Yqu_ho_.mjs.map +0 -1
- package/lib/browser/handleRequest-Cz4_wmQ9.cjs +0 -189
- package/lib/browser/handleRequest-Cz4_wmQ9.cjs.map +0 -1
- package/lib/browser/hasConfigurableGlobal-C8zq1MCg.mjs +0 -109
- package/lib/browser/hasConfigurableGlobal-C8zq1MCg.mjs.map +0 -1
- package/lib/browser/hasConfigurableGlobal-CS7adEvV.cjs +0 -126
- package/lib/browser/hasConfigurableGlobal-CS7adEvV.cjs.map +0 -1
- package/lib/browser/index.cjs +0 -70
- package/lib/browser/index.cjs.map +0 -1
- package/lib/browser/index.d.mts +0 -125
- package/lib/browser/index.mjs +0 -56
- package/lib/browser/index.mjs.map +0 -1
- package/lib/browser/interceptors/WebSocket/index.cjs +0 -618
- package/lib/browser/interceptors/WebSocket/index.cjs.map +0 -1
- package/lib/browser/interceptors/WebSocket/index.mjs.map +0 -1
- package/lib/browser/interceptors/XMLHttpRequest/index.cjs +0 -8
- package/lib/browser/interceptors/XMLHttpRequest/index.d.cts +0 -15
- package/lib/browser/interceptors/XMLHttpRequest/index.d.mts +0 -15
- package/lib/browser/interceptors/XMLHttpRequest/index.mjs +0 -8
- package/lib/browser/interceptors/fetch/index.cjs +0 -7
- package/lib/browser/interceptors/fetch/index.d.cts +0 -13
- package/lib/browser/interceptors/fetch/index.d.mts +0 -13
- package/lib/browser/interceptors/fetch/index.mjs +0 -7
- package/lib/browser/presets/browser.cjs +0 -18
- package/lib/browser/presets/browser.cjs.map +0 -1
- package/lib/browser/presets/browser.d.cts +0 -12
- package/lib/browser/presets/browser.d.mts +0 -14
- package/lib/browser/presets/browser.mjs +0 -18
- package/lib/browser/presets/browser.mjs.map +0 -1
- package/lib/browser/resolveWebSocketUrl-6K6EgqsA.cjs +0 -31
- package/lib/browser/resolveWebSocketUrl-6K6EgqsA.cjs.map +0 -1
- package/lib/browser/resolveWebSocketUrl-C83-x9iE.mjs.map +0 -1
- package/lib/node/BatchInterceptor-DdMNfSUY.mjs +0 -44
- package/lib/node/BatchInterceptor-DdMNfSUY.mjs.map +0 -1
- package/lib/node/BatchInterceptor-KKF8Q47I.d.cts +0 -26
- package/lib/node/BatchInterceptor-Z1lwK23r.cjs +0 -49
- package/lib/node/BatchInterceptor-Z1lwK23r.cjs.map +0 -1
- package/lib/node/BatchInterceptor-rSr-NH_H.d.mts +0 -26
- package/lib/node/ClientRequest-DFs7FPNi.cjs +0 -1058
- package/lib/node/ClientRequest-DFs7FPNi.cjs.map +0 -1
- package/lib/node/ClientRequest-dRpI9v1o.mjs +0 -1049
- package/lib/node/ClientRequest-dRpI9v1o.mjs.map +0 -1
- package/lib/node/Interceptor-BsZ21ue0.d.cts +0 -132
- package/lib/node/Interceptor-soD-WpQO.d.mts +0 -132
- package/lib/node/RemoteHttpInterceptor.cjs +0 -155
- package/lib/node/RemoteHttpInterceptor.cjs.map +0 -1
- package/lib/node/RemoteHttpInterceptor.d.mts +0 -39
- package/lib/node/RemoteHttpInterceptor.mjs.map +0 -1
- package/lib/node/XMLHttpRequest-DhupxLHX.mjs.map +0 -1
- package/lib/node/XMLHttpRequest-Dyi6ahGz.cjs +0 -752
- package/lib/node/XMLHttpRequest-Dyi6ahGz.cjs.map +0 -1
- package/lib/node/bufferUtils-DxPxwff_.mjs +0 -20
- package/lib/node/bufferUtils-DxPxwff_.mjs.map +0 -1
- package/lib/node/bufferUtils-S5_-2eN4.cjs +0 -38
- package/lib/node/bufferUtils-S5_-2eN4.cjs.map +0 -1
- package/lib/node/chunk-CbDLau6x.cjs +0 -34
- package/lib/node/fetch-BqMcbuYR.mjs +0 -252
- package/lib/node/fetch-BqMcbuYR.mjs.map +0 -1
- package/lib/node/fetch-aQJu1Gf_.cjs +0 -259
- package/lib/node/fetch-aQJu1Gf_.cjs.map +0 -1
- package/lib/node/fetchUtils-BKJ1XmiO.mjs +0 -480
- package/lib/node/fetchUtils-BKJ1XmiO.mjs.map +0 -1
- package/lib/node/fetchUtils-umV5xXBy.cjs +0 -546
- package/lib/node/fetchUtils-umV5xXBy.cjs.map +0 -1
- package/lib/node/getRawRequest-B6znU6il.cjs +0 -36
- package/lib/node/getRawRequest-B6znU6il.cjs.map +0 -1
- package/lib/node/getRawRequest-C2-1urzA.mjs +0 -24
- package/lib/node/getRawRequest-C2-1urzA.mjs.map +0 -1
- package/lib/node/handleRequest-DCLzePtS.mjs +0 -190
- package/lib/node/handleRequest-DCLzePtS.mjs.map +0 -1
- package/lib/node/handleRequest-DVOthWJo.cjs +0 -220
- package/lib/node/handleRequest-DVOthWJo.cjs.map +0 -1
- package/lib/node/hasConfigurableGlobal-BiTmog1u.mjs.map +0 -1
- package/lib/node/hasConfigurableGlobal-MjY06_Ok.cjs +0 -108
- package/lib/node/hasConfigurableGlobal-MjY06_Ok.cjs.map +0 -1
- package/lib/node/index-BJ5E5aW-.d.cts +0 -113
- package/lib/node/index-DfoF45ih.d.mts +0 -113
- package/lib/node/index.cjs +0 -53
- package/lib/node/index.cjs.map +0 -1
- package/lib/node/index.d.cts +0 -104
- package/lib/node/index.d.mts +0 -104
- package/lib/node/index.mjs.map +0 -1
- package/lib/node/interceptors/ClientRequest/index.cjs +0 -6
- package/lib/node/interceptors/ClientRequest/index.d.cts +0 -2
- package/lib/node/interceptors/ClientRequest/index.d.mts +0 -3
- package/lib/node/interceptors/ClientRequest/index.mjs +0 -6
- package/lib/node/interceptors/XMLHttpRequest/index.cjs +0 -7
- package/lib/node/interceptors/XMLHttpRequest/index.d.cts +0 -14
- package/lib/node/interceptors/XMLHttpRequest/index.d.mts +0 -14
- package/lib/node/interceptors/XMLHttpRequest/index.mjs +0 -7
- package/lib/node/interceptors/fetch/index.cjs +0 -6
- package/lib/node/interceptors/fetch/index.d.cts +0 -12
- package/lib/node/interceptors/fetch/index.d.mts +0 -12
- package/lib/node/interceptors/fetch/index.mjs +0 -6
- package/lib/node/node-DIKcnzhK.cjs +0 -39
- package/lib/node/node-DIKcnzhK.cjs.map +0 -1
- package/lib/node/node-lsdNwZEW.mjs +0 -27
- package/lib/node/node-lsdNwZEW.mjs.map +0 -1
- package/lib/node/presets/node.cjs +0 -23
- package/lib/node/presets/node.cjs.map +0 -1
- package/lib/node/presets/node.d.cts +0 -13
- package/lib/node/presets/node.d.mts +0 -15
- package/lib/node/presets/node.mjs +0 -23
- package/lib/node/presets/node.mjs.map +0 -1
- package/lib/node/utils/node/index.cjs +0 -4
- package/lib/node/utils/node/index.d.cts +0 -16
- package/lib/node/utils/node/index.d.mts +0 -16
- package/lib/node/utils/node/index.mjs +0 -3
- package/presets/browser/package.json +0 -5
- package/presets/node/package.json +0 -11
- package/src/BatchInterceptor.test.ts +0 -255
- package/src/BatchInterceptor.ts +0 -95
- package/src/Interceptor.test.ts +0 -205
- package/src/Interceptor.ts +0 -249
- package/src/getRawRequest.ts +0 -21
- package/src/interceptors/ClientRequest/MockHttpSocket.ts +0 -724
- package/src/interceptors/ClientRequest/agents.ts +0 -110
- package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.test.ts +0 -54
- package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts +0 -45
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts +0 -427
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts +0 -268
- package/src/interceptors/ClientRequest/utils/parserUtils.ts +0 -48
- package/src/interceptors/Socket/MockSocket.test.ts +0 -264
- package/src/interceptors/Socket/MockSocket.ts +0 -58
- package/src/interceptors/Socket/utils/baseUrlFromConnectionOptions.ts +0 -26
- package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.test.ts +0 -52
- package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.ts +0 -33
- package/src/interceptors/XMLHttpRequest/index.ts +0 -40
- package/src/interceptors/fetch/index.ts +0 -203
- package/src/utils/bufferUtils.ts +0 -22
- package/src/utils/canParseUrl.ts +0 -13
- package/src/utils/emitAsync.ts +0 -25
- package/src/utils/nextTick.ts +0 -11
- package/src/utils/node/index.ts +0 -39
- /package/src/{createRequestId.ts → create-request-id.ts} +0 -0
- /package/src/{InterceptorError.ts → interceptor-error.ts} +0 -0
- /package/src/interceptors/WebSocket/utils/{bindEvent.ts → bind-event.ts} +0 -0
- /package/src/interceptors/XMLHttpRequest/polyfills/{EventPolyfill.ts → event-polyfill.ts} +0 -0
- /package/src/interceptors/XMLHttpRequest/utils/{getBodyByteLength.ts → get-body-byte-length.ts} +0 -0
- /package/src/interceptors/XMLHttpRequest/utils/{isDomParserSupportedType.ts → is-dom-parser-supported-type.ts} +0 -0
- /package/src/interceptors/fetch/utils/{createNetworkError.ts → create-network-error.ts} +0 -0
- /package/src/utils/{findPropertySource.ts → find-property-source.ts} +0 -0
- /package/src/utils/{getCleanUrl.ts → get-clean-url.ts} +0 -0
- /package/src/utils/{getValueBySymbol.ts → get-value-by-symbol.ts} +0 -0
- /package/src/utils/{isNodeLikeError.ts → is-node-like-error.ts} +0 -0
- /package/src/utils/{isObject.ts → is-object.ts} +0 -0
- /package/src/utils/{isPropertyAccessible.ts → is-property-accessible.ts} +0 -0
- /package/src/utils/{parseJson.ts → parse-json.ts} +0 -0
- /package/src/utils/{resolveWebSocketUrl.ts → resolve-web-socket-url.ts} +0 -0
|
@@ -0,0 +1,1388 @@
|
|
|
1
|
+
import { a as createLogger, i as Interceptor, r as toBuffer } from "./buffer-utils-BvPY1Tc-.js";
|
|
2
|
+
import { TypedEvent } from "rettime";
|
|
3
|
+
import { invariant } from "outvariant";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import net from "node:net";
|
|
6
|
+
import tls from "node:tls";
|
|
7
|
+
//#region src/utils/patches-registry.ts
|
|
8
|
+
var PatchesRegistry = class {
|
|
9
|
+
#replacements = /* @__PURE__ */ new Map();
|
|
10
|
+
applyPatch(owner, key, getNextValue) {
|
|
11
|
+
const ownerReplacements = this.#replacements.get(owner);
|
|
12
|
+
invariant(!ownerReplacements?.has(key), `Failed to replace a global value at "${String(key)}": already replaced.`);
|
|
13
|
+
const match = getDeepPropertyDescriptor(owner, key);
|
|
14
|
+
if (typeof match === "undefined") {
|
|
15
|
+
console.warn(`Failed to replace a global value at "${String(key)}": not a global value.`);
|
|
16
|
+
return () => {};
|
|
17
|
+
}
|
|
18
|
+
if (match.descriptor.configurable) Object.defineProperty(owner, key, {
|
|
19
|
+
value: getNextValue(owner[key]),
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true
|
|
22
|
+
});
|
|
23
|
+
else if (match.descriptor.writable) owner[key] = getNextValue(owner[key]);
|
|
24
|
+
else throw new Error(`Failed to patch a non-configurable non-writable property "${key.toString()}"`);
|
|
25
|
+
const restorePatch = () => {
|
|
26
|
+
const currentReplacements = this.#replacements.get(owner);
|
|
27
|
+
if (!currentReplacements?.has(key)) return;
|
|
28
|
+
if (match.owner === owner)
|
|
29
|
+
/**
|
|
30
|
+
* @note Restoring non-configurable properties works as long as "writable: true"
|
|
31
|
+
* and none of the other descriptor properties except for "value" have changed.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(match.owner, key, match.descriptor);
|
|
34
|
+
else
|
|
35
|
+
/**
|
|
36
|
+
* @todo Delete the proxy property set by the registry.
|
|
37
|
+
* If the match's owner isn't the original owner, the property is likely nested in the prototype.
|
|
38
|
+
* The registry does not meddle with those, they are left intact.
|
|
39
|
+
*/
|
|
40
|
+
Reflect.deleteProperty(owner, key);
|
|
41
|
+
currentReplacements.delete(key);
|
|
42
|
+
if (currentReplacements.size === 0) this.#replacements.delete(owner);
|
|
43
|
+
};
|
|
44
|
+
if (ownerReplacements) ownerReplacements.set(key, restorePatch);
|
|
45
|
+
else this.#replacements.set(owner, /* @__PURE__ */ new Map([[key, restorePatch]]));
|
|
46
|
+
return restorePatch;
|
|
47
|
+
}
|
|
48
|
+
restoreAllPatches() {
|
|
49
|
+
const errors = [];
|
|
50
|
+
for (const [, ownerReplacements] of this.#replacements) for (const [, restorePatch] of ownerReplacements) try {
|
|
51
|
+
restorePatch();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error instanceof Error) errors.push(error);
|
|
54
|
+
else throw error;
|
|
55
|
+
}
|
|
56
|
+
if (errors.length > 0) throw new AggregateError(errors, "FOO!");
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const patchesRegistry = new PatchesRegistry();
|
|
60
|
+
/**
|
|
61
|
+
* Returns a property descriptor for the given property on the owner.
|
|
62
|
+
* Walks down the prototype chain if the property does not exist on the owner.
|
|
63
|
+
* Handy for getting a global property descriptor where `globalThis` is
|
|
64
|
+
* replaced with a controlled class (e.g. ServiceWorkerGlobalScope).
|
|
65
|
+
*/
|
|
66
|
+
function getDeepPropertyDescriptor(owner, key) {
|
|
67
|
+
let currentOwner = owner;
|
|
68
|
+
let descriptor;
|
|
69
|
+
while (currentOwner) {
|
|
70
|
+
descriptor = Object.getOwnPropertyDescriptor(currentOwner, key);
|
|
71
|
+
if (descriptor) return {
|
|
72
|
+
owner: currentOwner,
|
|
73
|
+
descriptor
|
|
74
|
+
};
|
|
75
|
+
currentOwner = Object.getPrototypeOf(currentOwner);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/interceptors/net/utils/normalize-net-connect-args.ts
|
|
80
|
+
/**
|
|
81
|
+
* Normalizes the arguments passed to `net.connect()`.
|
|
82
|
+
*/
|
|
83
|
+
function normalizeNetConnectArgs(args) {
|
|
84
|
+
if (args.length === 0) return [{ path: "" }, null];
|
|
85
|
+
const callback = typeof args[1] === "function" ? args[1] : args[2] || null;
|
|
86
|
+
if (typeof args[0] === "string") return [{ path: args[0] }, callback];
|
|
87
|
+
if (typeof args[0] === "number") return [{
|
|
88
|
+
port: args[0],
|
|
89
|
+
path: "",
|
|
90
|
+
/**
|
|
91
|
+
* @note The host is optional in the "(port[, host][, callback])"
|
|
92
|
+
* signatures and defaults to "localhost" in Node.js.
|
|
93
|
+
*/
|
|
94
|
+
host: typeof args[1] === "string" ? args[1] : void 0
|
|
95
|
+
}, callback];
|
|
96
|
+
if (typeof args[0] === "object") {
|
|
97
|
+
/**
|
|
98
|
+
* @note URL arguments receive no special treatment on purpose.
|
|
99
|
+
* Node.js does not support them and reads a given URL as a plain
|
|
100
|
+
* options object (e.g. "url.port" is a string, "url.host" includes
|
|
101
|
+
* the port). The "port" branch below reproduces that reading.
|
|
102
|
+
*/
|
|
103
|
+
if ("port" in args[0]) return [{
|
|
104
|
+
path: "",
|
|
105
|
+
port: Reflect.get(args[0], "port"),
|
|
106
|
+
host: Reflect.get(args[0], "host"),
|
|
107
|
+
auth: Reflect.get(args[0], "auth"),
|
|
108
|
+
family: Reflect.get(args[0], "family"),
|
|
109
|
+
hints: Reflect.get(args[0], "hints"),
|
|
110
|
+
session: Reflect.get(args[0], "session"),
|
|
111
|
+
localAddress: Reflect.get(args[0], "localAddress"),
|
|
112
|
+
localPort: Reflect.get(args[0], "localPort"),
|
|
113
|
+
timeout: Reflect.get(args[0], "timeout"),
|
|
114
|
+
lookup: Reflect.get(args[0], "lookup"),
|
|
115
|
+
allowHalfOpen: Reflect.get(args[0], "allowHalfOpen"),
|
|
116
|
+
noDelay: Reflect.get(args[0], "noDelay"),
|
|
117
|
+
keepAlive: Reflect.get(args[0], "keepAlive"),
|
|
118
|
+
keepAliveInitialDelay: Reflect.get(args[0], "keepAliveInitialDelay"),
|
|
119
|
+
autoSelectFamily: Reflect.get(args[0], "autoSelectFamily"),
|
|
120
|
+
autoSelectFamilyAttemptTimeout: Reflect.get(args[0], "autoSelectFamilyAttemptTimeout")
|
|
121
|
+
}, callback];
|
|
122
|
+
return [{
|
|
123
|
+
path: args[0].path || "",
|
|
124
|
+
family: Reflect.get(args[0], "family"),
|
|
125
|
+
session: Reflect.get(args[0], "session"),
|
|
126
|
+
auth: Reflect.get(args[0], "auth"),
|
|
127
|
+
timeout: Reflect.get(args[0], "timeout"),
|
|
128
|
+
allowHalfOpen: Reflect.get(args[0], "allowHalfOpen")
|
|
129
|
+
}, callback];
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`Invalid arguments passed to net.connect: ${args}`);
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/interceptors/net/utils/flush-writes.ts
|
|
135
|
+
/**
|
|
136
|
+
* Write the given pending data to the socket using its public
|
|
137
|
+
* "write()" method. Unlike direct "_writeGeneric()" calls, public
|
|
138
|
+
* writes go through the "Writable" queue that only dispatches the
|
|
139
|
+
* next write once the previous one completes. TLS sockets rely on
|
|
140
|
+
* that serialization: their "TLSWrap" handle supports a single
|
|
141
|
+
* write in flight and crashes the process with a native
|
|
142
|
+
* "Assertion failed: !current_write_" abort when writes overlap
|
|
143
|
+
* (e.g. multiple direct "_writeGeneric()" calls issued on a
|
|
144
|
+
* connecting socket replay back-to-back on "connect").
|
|
145
|
+
*/
|
|
146
|
+
function writePendingData(socket, data, encoding, callback) {
|
|
147
|
+
if (Array.isArray(data)) {
|
|
148
|
+
for (let index = 0; index < data.length; index++) {
|
|
149
|
+
const entry = data[index];
|
|
150
|
+
/**
|
|
151
|
+
* @note Per-entry callbacks are intentionally ignored, the same
|
|
152
|
+
* way "writevGeneric()" only honors the batch-level callback.
|
|
153
|
+
*/
|
|
154
|
+
if (index === data.length - 1) socket.write(entry.chunk, entry.encoding, callback);
|
|
155
|
+
else socket.write(entry.chunk, entry.encoding);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
socket.write(data, encoding, callback);
|
|
160
|
+
}
|
|
161
|
+
function unwrapPendingData(data, callback) {
|
|
162
|
+
if (Array.isArray(data)) for (const entry of data) callback(entry.chunk, entry.encoding, entry.callback);
|
|
163
|
+
else callback(data);
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/interceptors/net/utils/get-tls-connect-options.ts
|
|
167
|
+
/**
|
|
168
|
+
* Returns the original options the given TLS socket was created with.
|
|
169
|
+
* The original `tls.connect()` stores them on the socket instance
|
|
170
|
+
* under an internal symbol before connecting its transport.
|
|
171
|
+
* @see https://github.com/nodejs/node/blob/3178a762d6a2b1a37b74f02266eea0f3d86603f1/lib/_tls_wrap.js#L1690
|
|
172
|
+
*/
|
|
173
|
+
function getTlsConnectOptions(socket) {
|
|
174
|
+
const kConnectOptions = Object.getOwnPropertySymbols(socket).find((symbol) => {
|
|
175
|
+
return symbol.description === "connect-options";
|
|
176
|
+
});
|
|
177
|
+
if (kConnectOptions == null) return;
|
|
178
|
+
return Reflect.get(socket, kConnectOptions);
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/interceptors/net/utils/address-info.ts
|
|
182
|
+
function getAddressInfoByConnectionOptions(options) {
|
|
183
|
+
if (options == null) return {};
|
|
184
|
+
const isIPv6 = options.family === 6 || net.isIPv6(options.host || "");
|
|
185
|
+
return {
|
|
186
|
+
address: isIPv6 ? "::1" : "127.0.0.1",
|
|
187
|
+
/**
|
|
188
|
+
* @note Coerce the port to a number. Connection options may
|
|
189
|
+
* describe it as a string (e.g. when created from a URL), while
|
|
190
|
+
* the address info always reports a numeric port.
|
|
191
|
+
*/
|
|
192
|
+
port: Number(options.port) || (options.protocol === "https:" ? 443 : 80),
|
|
193
|
+
family: isIPv6 ? "IPv6" : "IPv4"
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get the local address info for the given connection options.
|
|
198
|
+
* This describes the client-side end of the connection: the socket
|
|
199
|
+
* is bound to the loopback interface and an ephemeral port, the same
|
|
200
|
+
* way the operating system binds an outgoing connection.
|
|
201
|
+
*/
|
|
202
|
+
function getLocalAddressInfoByConnectionOptions(options) {
|
|
203
|
+
if (options == null) return {};
|
|
204
|
+
const isIPv6 = options.family === 6 || net.isIPv6(options.host || "");
|
|
205
|
+
return {
|
|
206
|
+
address: options.localAddress || (isIPv6 ? "::1" : "127.0.0.1"),
|
|
207
|
+
port: options.localPort || getEphemeralPort(),
|
|
208
|
+
family: isIPv6 ? "IPv6" : "IPv4"
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Get a random port from the ephemeral range (IANA: 49152-65535),
|
|
213
|
+
* the range the operating system draws from when binding an
|
|
214
|
+
* outgoing connection.
|
|
215
|
+
*/
|
|
216
|
+
function getEphemeralPort() {
|
|
217
|
+
return 49152 + Math.floor(Math.random() * 16384);
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/interceptors/net/socket-controller.ts
|
|
221
|
+
const kListenerWrap = Symbol("kListenerWrap");
|
|
222
|
+
const kRawSocket = Symbol("kRawSocket");
|
|
223
|
+
const kPatched = Symbol("kPatched");
|
|
224
|
+
const logger$1 = createLogger("socket");
|
|
225
|
+
function toServerSocket(socket) {
|
|
226
|
+
/**
|
|
227
|
+
* The server-side write buffer. Pushing data to the client honors
|
|
228
|
+
* the client's read backpressure: once "socket.push()" reports a
|
|
229
|
+
* full buffer, subsequent server writes queue here and flush when
|
|
230
|
+
* the client reads again ("_read"). This mirrors how a real server
|
|
231
|
+
* cannot write faster than the client consumes.
|
|
232
|
+
*/
|
|
233
|
+
const pendingWrites = [];
|
|
234
|
+
let pendingEnd;
|
|
235
|
+
let isBackpressured = false;
|
|
236
|
+
let isFlushScheduled = false;
|
|
237
|
+
const flushPendingWrites = () => {
|
|
238
|
+
/**
|
|
239
|
+
* @note The mocked connection is not established yet. Flush once
|
|
240
|
+
* it is, the same way a real server cannot write to a client
|
|
241
|
+
* that has not connected.
|
|
242
|
+
*/
|
|
243
|
+
if (socket.connecting) {
|
|
244
|
+
isBackpressured = true;
|
|
245
|
+
if (!isFlushScheduled) {
|
|
246
|
+
isFlushScheduled = true;
|
|
247
|
+
socket.once("ready", () => {
|
|
248
|
+
isFlushScheduled = false;
|
|
249
|
+
flushPendingWrites();
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
while (pendingWrites.length > 0) {
|
|
255
|
+
const nextWrite = pendingWrites.shift();
|
|
256
|
+
socket._unrefTimer();
|
|
257
|
+
const canPushMore = socket.push(toBuffer(nextWrite.chunk, nextWrite.encoding), nextWrite.encoding);
|
|
258
|
+
nextWrite.callback?.();
|
|
259
|
+
if (!canPushMore) {
|
|
260
|
+
isBackpressured = true;
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const wasBackpressured = isBackpressured;
|
|
265
|
+
isBackpressured = false;
|
|
266
|
+
if (pendingEnd) {
|
|
267
|
+
const finalEnd = pendingEnd;
|
|
268
|
+
pendingEnd = void 0;
|
|
269
|
+
if (finalEnd.chunk != null) socket.push(toBuffer(finalEnd.chunk, finalEnd.encoding), finalEnd.encoding);
|
|
270
|
+
socket.push(null);
|
|
271
|
+
finalEnd.callback?.();
|
|
272
|
+
}
|
|
273
|
+
if (wasBackpressured) socket.emit("internal:drain");
|
|
274
|
+
return true;
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* @note "_read" is the client asking for more data. Flush the
|
|
278
|
+
* buffered server writes so the delivery resumes as the client
|
|
279
|
+
* reads, completing the backpressure loop.
|
|
280
|
+
*/
|
|
281
|
+
const realRead = socket._read.bind(socket);
|
|
282
|
+
socket._read = (size) => {
|
|
283
|
+
realRead(size);
|
|
284
|
+
if (pendingWrites.length > 0 || pendingEnd != null || isBackpressured) flushPendingWrites();
|
|
285
|
+
};
|
|
286
|
+
return new Proxy(socket, { get: (target, property, receiver) => {
|
|
287
|
+
const getRealValue = () => {
|
|
288
|
+
return Reflect.get(target, property, receiver);
|
|
289
|
+
};
|
|
290
|
+
if (property === "on" || property === "addListener" || property === "once" || property === "prependListener" || property === "prependOnceListener") {
|
|
291
|
+
const realAddListener = getRealValue();
|
|
292
|
+
return (event, listener) => {
|
|
293
|
+
if (event === "data") {
|
|
294
|
+
const listenerWrap = (chunk, encoding) => {
|
|
295
|
+
listener(toBuffer(chunk, encoding));
|
|
296
|
+
};
|
|
297
|
+
Object.defineProperty(listener, kListenerWrap, {
|
|
298
|
+
enumerable: false,
|
|
299
|
+
writable: false,
|
|
300
|
+
value: listenerWrap
|
|
301
|
+
});
|
|
302
|
+
/**
|
|
303
|
+
* @note Subscribe using the same method (e.g. "once") so its
|
|
304
|
+
* listener semantics apply to the internal channel too.
|
|
305
|
+
*/
|
|
306
|
+
Reflect.apply(realAddListener, target, ["internal:write", listenerWrap]);
|
|
307
|
+
return target;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* @note The "drain" event on the server socket signals the
|
|
311
|
+
* flush of the server-side write buffer. It is routed through
|
|
312
|
+
* an internal channel so it does not clash with the "drain"
|
|
313
|
+
* event of the underlying client socket.
|
|
314
|
+
*/
|
|
315
|
+
if (event === "drain") {
|
|
316
|
+
Reflect.apply(realAddListener, target, ["internal:drain", listener]);
|
|
317
|
+
return target;
|
|
318
|
+
}
|
|
319
|
+
return realAddListener.call(target, event, listener);
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (property === "off" || property === "removeListener") {
|
|
323
|
+
const realRemoveListener = getRealValue();
|
|
324
|
+
return (event, listener) => {
|
|
325
|
+
if (event === "data") {
|
|
326
|
+
const listenerWrap = listener[kListenerWrap];
|
|
327
|
+
if (listenerWrap) return realRemoveListener.call(target, "internal:write", listenerWrap);
|
|
328
|
+
}
|
|
329
|
+
if (event === "drain") return realRemoveListener.call(target, "internal:drain", listener);
|
|
330
|
+
return realRemoveListener.call(target, event, listener);
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (property === "write") return ((chunk, encoding, callback) => {
|
|
334
|
+
if (typeof encoding === "function") {
|
|
335
|
+
callback = encoding;
|
|
336
|
+
encoding = void 0;
|
|
337
|
+
}
|
|
338
|
+
pendingWrites.push({
|
|
339
|
+
chunk,
|
|
340
|
+
encoding,
|
|
341
|
+
callback
|
|
342
|
+
});
|
|
343
|
+
/**
|
|
344
|
+
* @note Do not push more data to a client that is not
|
|
345
|
+
* reading. The write stays buffered until the client reads
|
|
346
|
+
* again, and "drain" signals the flush.
|
|
347
|
+
*/
|
|
348
|
+
if (isBackpressured) return false;
|
|
349
|
+
return flushPendingWrites();
|
|
350
|
+
});
|
|
351
|
+
if (property === "end") return ((...args) => {
|
|
352
|
+
const callback = args[args.length - 1];
|
|
353
|
+
/**
|
|
354
|
+
* @note The end-of-stream is delivered once the buffered
|
|
355
|
+
* writes flush so the client never observes the EOF before
|
|
356
|
+
* the data that preceded it.
|
|
357
|
+
*/
|
|
358
|
+
pendingEnd = {
|
|
359
|
+
chunk: typeof args[0] === "function" ? void 0 : args[0],
|
|
360
|
+
encoding: typeof args[1] === "string" ? args[1] : void 0,
|
|
361
|
+
callback: typeof callback === "function" ? callback : void 0
|
|
362
|
+
};
|
|
363
|
+
flushPendingWrites();
|
|
364
|
+
/**
|
|
365
|
+
* @note Do not end the client socket's writable side.
|
|
366
|
+
* The server ending the connection only signals EOF to the
|
|
367
|
+
* client (the client may keep writing on half-open sockets).
|
|
368
|
+
*/
|
|
369
|
+
return target;
|
|
370
|
+
});
|
|
371
|
+
return getRealValue();
|
|
372
|
+
} });
|
|
373
|
+
}
|
|
374
|
+
var SocketController = class SocketController {
|
|
375
|
+
static {
|
|
376
|
+
this.PENDING = 0;
|
|
377
|
+
}
|
|
378
|
+
static {
|
|
379
|
+
this.CLAIMED = 1;
|
|
380
|
+
}
|
|
381
|
+
static {
|
|
382
|
+
this.PASSTHROUGH = 2;
|
|
383
|
+
}
|
|
384
|
+
constructor(socket) {
|
|
385
|
+
this[kRawSocket] = socket;
|
|
386
|
+
socket[kPatched] = true;
|
|
387
|
+
this.readyState = SocketController.PENDING;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Claim this socket. Once claimed, the connection attempt succeeds
|
|
391
|
+
* regardless of the requested host and the interceptor becomes the
|
|
392
|
+
* mocked server for this connection.
|
|
393
|
+
*/
|
|
394
|
+
claim() {
|
|
395
|
+
invariant(this.readyState === SocketController.PENDING, "Failed to claim a socket connection: already handled (%s)", this.readyState);
|
|
396
|
+
this.readyState = SocketController.CLAIMED;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Establish this socket connection as-is.
|
|
400
|
+
*/
|
|
401
|
+
passthrough() {
|
|
402
|
+
invariant(this.readyState === SocketController.PENDING, "Failed to passthrough a socket connection: already handled (%s)", this.readyState);
|
|
403
|
+
this.readyState = SocketController.PASSTHROUGH;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
var TcpSocketController = class extends SocketController {
|
|
407
|
+
#connectionOptions;
|
|
408
|
+
#realWriteGeneric;
|
|
409
|
+
#passthroughSocket = null;
|
|
410
|
+
#bufferedWrites = [];
|
|
411
|
+
#readsCorked = false;
|
|
412
|
+
#corkedReads = [];
|
|
413
|
+
#realHandleSwapped = false;
|
|
414
|
+
#clientCloseEmitted = false;
|
|
415
|
+
#clientEndPushed = false;
|
|
416
|
+
#connectEmulated = false;
|
|
417
|
+
constructor(socket, createConnection, connectionOptions) {
|
|
418
|
+
super(socket);
|
|
419
|
+
this.socket = socket;
|
|
420
|
+
this.createConnection = createConnection;
|
|
421
|
+
/**
|
|
422
|
+
* @note Plain TCP sockets capture the connection options from the
|
|
423
|
+
* "socket.connect()" proxy below. TLS sockets carry additional
|
|
424
|
+
* TLS-level options that never pass through "socket.connect()",
|
|
425
|
+
* so those are provided explicitly (see "TlsSocketController").
|
|
426
|
+
*/
|
|
427
|
+
this.#connectionOptions = connectionOptions;
|
|
428
|
+
this.socket._read = () => {
|
|
429
|
+
/**
|
|
430
|
+
* @note Resume the passthrough socket when the consumer asks for
|
|
431
|
+
* more data. Node.js calls "_read()" once the consumer drains the
|
|
432
|
+
* read buffer (e.g. after "resume()"). The passthrough socket may
|
|
433
|
+
* have been paused when the consumer's buffer got full, and this
|
|
434
|
+
* is the only signal to continue reading.
|
|
435
|
+
*/
|
|
436
|
+
this.#passthroughSocket?.resume();
|
|
437
|
+
};
|
|
438
|
+
this.#realWriteGeneric = this.socket._writeGeneric;
|
|
439
|
+
this.#bufferedWrites = [];
|
|
440
|
+
this.socket._writeGeneric = (...args) => {
|
|
441
|
+
this.#handleWrite(args);
|
|
442
|
+
};
|
|
443
|
+
this.socket.connect = new Proxy(this.socket.connect, { apply: (target, thisArg, args) => {
|
|
444
|
+
logger$1.verbose("socket.connect() %o", args);
|
|
445
|
+
this.#connectionOptions = args[0];
|
|
446
|
+
/**
|
|
447
|
+
* @note Do not bind the intercepted socket to the requested
|
|
448
|
+
* local address/port. Binding reserves the port for real, and
|
|
449
|
+
* the passthrough connection (created with the original options)
|
|
450
|
+
* would then bind the same port again, resulting in a conflict.
|
|
451
|
+
* The requested values are still reflected in the address info
|
|
452
|
+
* of a claimed socket (see "claim()").
|
|
453
|
+
*/
|
|
454
|
+
if (args[0] != null && typeof args[0] === "object" && (args[0].localAddress != null || args[0].localPort != null)) args[0] = {
|
|
455
|
+
...args[0],
|
|
456
|
+
localAddress: void 0,
|
|
457
|
+
localPort: void 0
|
|
458
|
+
};
|
|
459
|
+
return Reflect.apply(target, thisArg, args);
|
|
460
|
+
} });
|
|
461
|
+
/**
|
|
462
|
+
* @note A single socket can be reused for connections to the same host.
|
|
463
|
+
* When one connection ends, the Agent frees the socket, then uses it
|
|
464
|
+
* to write the next request's HTTP message immediately. Use the "free"
|
|
465
|
+
* event to transition the controller into the pending state so the
|
|
466
|
+
* next exchange is handled anew.
|
|
467
|
+
*/
|
|
468
|
+
socket.on("free", () => {
|
|
469
|
+
logger$1.verbose("client socket freed!");
|
|
470
|
+
this.reset();
|
|
471
|
+
}).on("close", () => {
|
|
472
|
+
logger$1.verbose("client socket closed!");
|
|
473
|
+
this.#clientCloseEmitted = true;
|
|
474
|
+
/**
|
|
475
|
+
* @note Destroy the passthrough socket, if any. The client
|
|
476
|
+
* socket is done, and a passthrough connection left open would
|
|
477
|
+
* linger until the server closes it (or error unhandled if it
|
|
478
|
+
* is still connecting).
|
|
479
|
+
*/
|
|
480
|
+
this.#passthroughSocket?.destroy();
|
|
481
|
+
this.#passthroughSocket = null;
|
|
482
|
+
this.#bufferedWrites = [];
|
|
483
|
+
this.#readsCorked = false;
|
|
484
|
+
this.#corkedReads = [];
|
|
485
|
+
});
|
|
486
|
+
this.serverSocket = toServerSocket(this.socket);
|
|
487
|
+
this.pendingConnection = Promise.withResolvers();
|
|
488
|
+
this.#reset();
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Reset this controller to the pending state so the next exchange
|
|
492
|
+
* on this socket can be handled anew. This is meant for kept-alive
|
|
493
|
+
* sockets that are reused for multiple exchanges by clients that
|
|
494
|
+
* don't emit the "free" event on the socket (e.g. Undici).
|
|
495
|
+
*/
|
|
496
|
+
reset() {
|
|
497
|
+
/**
|
|
498
|
+
* @note Only settled (claimed or passed-through) sockets need a reset.
|
|
499
|
+
* Resetting a pending socket again would discard the writes already
|
|
500
|
+
* buffered for the next exchange (e.g. the Agent "free" event firing
|
|
501
|
+
* after the parser has reset this controller at a message boundary).
|
|
502
|
+
*/
|
|
503
|
+
if (this.readyState === SocketController.PENDING) return;
|
|
504
|
+
this.#reset();
|
|
505
|
+
}
|
|
506
|
+
#reset() {
|
|
507
|
+
logger$1.verbose("resetting the socket...");
|
|
508
|
+
this.readyState = SocketController.PENDING;
|
|
509
|
+
this.pendingConnection = Promise.withResolvers();
|
|
510
|
+
this.#bufferedWrites = [];
|
|
511
|
+
this.#connectEmulated = false;
|
|
512
|
+
this.socket._pendingData = null;
|
|
513
|
+
this.socket._pendingEncoding = "";
|
|
514
|
+
const wrapHandle = (handle) => {
|
|
515
|
+
this.pendingConnection.promise.then(() => {
|
|
516
|
+
logger$1.verbose("connection request resolved!", this.readyState);
|
|
517
|
+
process.nextTick(() => {
|
|
518
|
+
/**
|
|
519
|
+
* @note If by this point the socket hasn't been handled,
|
|
520
|
+
* is still connecting, doesn't have any writes buffered,
|
|
521
|
+
* and has a "connect" listener, assume it's the "write after connect"
|
|
522
|
+
* scenario (e.g. undici). In that case, auto-claim the socket to
|
|
523
|
+
* transition to the connected state appropriately to its handle.
|
|
524
|
+
*/
|
|
525
|
+
if (this.readyState === SocketController.PENDING && this.socket.connecting && this.#bufferedWrites.length === 0 && this.socket.listenerCount("connect") > 0) {
|
|
526
|
+
logger$1.verbose("assume connect->write socket, calling \"connect\" listeners...");
|
|
527
|
+
this.emulateConnect();
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
/**
|
|
532
|
+
* Remove the "setTypeOfService" from the handle, if present (Node.js v24+).
|
|
533
|
+
* Removing it has no effect on the socket but prevents the "setTypeOfService EBADF" error.
|
|
534
|
+
* @see https://github.com/nodejs/node/blob/69a970f76814d40f55cf162d0cc3632fe8a7e599/lib/net.js#L661
|
|
535
|
+
* @see https://github.com/nodejs/undici/blob/bf684f7de01616708a33a5d1c092177622394442/lib/dispatcher/client-h1.js#L1136
|
|
536
|
+
*/
|
|
537
|
+
if (handle.setTypeOfService) handle.setTypeOfService = void 0;
|
|
538
|
+
handle.connect = handle.connect6 = (request) => {
|
|
539
|
+
logger$1.verbose("handle.connect()");
|
|
540
|
+
this.pendingConnection.resolve([request, handle]);
|
|
541
|
+
};
|
|
542
|
+
logger$1.verbose("socket handle wrapped! waiting for connection request...");
|
|
543
|
+
};
|
|
544
|
+
if (this.socket._handle) wrapHandle(this.socket._handle);
|
|
545
|
+
else this.socket.prependOnceListener("connectionAttempt", () => {
|
|
546
|
+
wrapHandle(this.socket._handle);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Handle a write performed on the client socket. Installed once as
|
|
551
|
+
* "_writeGeneric", this is the single write path for every controller
|
|
552
|
+
* state, dispatching on "readyState".
|
|
553
|
+
*/
|
|
554
|
+
#handleWrite(args) {
|
|
555
|
+
const data = args[1];
|
|
556
|
+
logger$1.verbose("socket write (state: %d) %o", this.readyState, args);
|
|
557
|
+
/**
|
|
558
|
+
* @note Buffer the write BEFORE pushing the data to the server socket.
|
|
559
|
+
* Handling the pushed data may transition this controller within the
|
|
560
|
+
* same call stack, and each transition settles the buffered entry:
|
|
561
|
+
* "passthrough()" flushes it to the real socket, "claim()" drops it,
|
|
562
|
+
* and a reset at an HTTP message boundary keeps it for the next exchange.
|
|
563
|
+
*/
|
|
564
|
+
this.#bufferedWrites.push(args);
|
|
565
|
+
if (this.readyState === SocketController.PENDING) {
|
|
566
|
+
/**
|
|
567
|
+
* @note Reflect the buffered write in "_pendingData" so it counts
|
|
568
|
+
* toward "bytesWritten", the same way Node.js counts the pending
|
|
569
|
+
* data of a connecting socket. Flushing the buffered writes resets
|
|
570
|
+
* "_pendingData" (see `passthrough()`).
|
|
571
|
+
*/
|
|
572
|
+
const pendingData = Array.isArray(this.socket._pendingData) ? this.socket._pendingData : [];
|
|
573
|
+
unwrapPendingData(data, (chunk, chunkEncoding) => {
|
|
574
|
+
pendingData.push({
|
|
575
|
+
chunk,
|
|
576
|
+
encoding: chunkEncoding
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
this.socket._pendingData = pendingData;
|
|
580
|
+
if (this.socket.listenerCount("internal:write") === 0) {
|
|
581
|
+
logger$1.verbose("no server data listeners, scheduling to the next tick...");
|
|
582
|
+
process.nextTick(() => {
|
|
583
|
+
this.#push(data);
|
|
584
|
+
});
|
|
585
|
+
} else this.#push(data);
|
|
586
|
+
} else this.#push(data);
|
|
587
|
+
/**
|
|
588
|
+
* Dispatch on the state observed AFTER the push since handling the
|
|
589
|
+
* pushed data may have transitioned this controller synchronously.
|
|
590
|
+
*/
|
|
591
|
+
switch (this.readyState) {
|
|
592
|
+
case SocketController.PENDING:
|
|
593
|
+
/**
|
|
594
|
+
* @note The write either awaits the verdict on this exchange or,
|
|
595
|
+
* if the push reset this controller at a message boundary, opens
|
|
596
|
+
* the next exchange (the reset cleared the buffer; re-buffer it).
|
|
597
|
+
*/
|
|
598
|
+
if (!this.#bufferedWrites.includes(args)) this.#bufferedWrites.push(args);
|
|
599
|
+
this.#acknowledgeWrite(args);
|
|
600
|
+
return;
|
|
601
|
+
case SocketController.CLAIMED:
|
|
602
|
+
/**
|
|
603
|
+
* @note Once claimed, there's nowhere else to write chunks to.
|
|
604
|
+
* The data was delivered to the server socket; complete the write
|
|
605
|
+
* so the socket's writable state settles (enabling "finish").
|
|
606
|
+
*/
|
|
607
|
+
this.#removeBufferedWrite(args);
|
|
608
|
+
this.#acknowledgeWrite(args);
|
|
609
|
+
return;
|
|
610
|
+
case SocketController.PASSTHROUGH:
|
|
611
|
+
/**
|
|
612
|
+
* @note A synchronous "passthrough()" has already flushed this
|
|
613
|
+
* write (with its callback) to the real socket.
|
|
614
|
+
*/
|
|
615
|
+
if (!this.#removeBufferedWrite(args)) return;
|
|
616
|
+
/**
|
|
617
|
+
* @note Until the handle swap, the client socket's own handle
|
|
618
|
+
* cannot carry any data (it never actually connects). Write
|
|
619
|
+
* directly to the passthrough socket instead. This also prevents
|
|
620
|
+
* the "connecting" write replay of `Socket.prototype._writeGeneric`
|
|
621
|
+
* from pushing the same data to the server socket twice.
|
|
622
|
+
*/
|
|
623
|
+
if (!this.#realHandleSwapped && this.#passthroughSocket) {
|
|
624
|
+
writePendingData(this.#passthroughSocket, data, args[2], args[3]);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
this.#realWriteGeneric.apply(this.socket, args);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Complete the given write by invoking its callback. The callback is
|
|
632
|
+
* then removed from the write entry so flushing that entry later
|
|
633
|
+
* (e.g. on passthrough) does not invoke it twice.
|
|
634
|
+
*/
|
|
635
|
+
#acknowledgeWrite(args) {
|
|
636
|
+
const callback = args[3];
|
|
637
|
+
if (typeof callback === "function") {
|
|
638
|
+
callback();
|
|
639
|
+
args[3] = void 0;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
#removeBufferedWrite(args) {
|
|
643
|
+
const index = this.#bufferedWrites.indexOf(args);
|
|
644
|
+
if (index === -1) return false;
|
|
645
|
+
this.#bufferedWrites.splice(index, 1);
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
emulateConnect() {
|
|
649
|
+
this.#connectEmulated = true;
|
|
650
|
+
/**
|
|
651
|
+
* @note Reflect the connected state before notifying the listeners,
|
|
652
|
+
* the same way Node.js does before emitting "connect".
|
|
653
|
+
*/
|
|
654
|
+
Reflect.set(this.socket, "connecting", false);
|
|
655
|
+
/**
|
|
656
|
+
* @note Invoke the raw listeners so the "once" wrappers get
|
|
657
|
+
* consumed. This prevents listeners like the connection callback
|
|
658
|
+
* from being invoked again when "connect" is emitted for real
|
|
659
|
+
* (e.g. once the claimed connection completes).
|
|
660
|
+
*/
|
|
661
|
+
for (const listener of this.socket.rawListeners("connect")) listener.apply(this.socket);
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Push the given data to the server socket.
|
|
665
|
+
* This has no effect on the public-facing socket and is used
|
|
666
|
+
* only for the interceptors to subscribe to "socket.on('data')"
|
|
667
|
+
* before the data is actually written anywhere.
|
|
668
|
+
*/
|
|
669
|
+
#push = (data) => {
|
|
670
|
+
if (data == null) return;
|
|
671
|
+
logger$1.verbose("server push %o", data);
|
|
672
|
+
unwrapPendingData(data, (chunk, encoding) => {
|
|
673
|
+
logger$1.verbose("server emitting \"data\" %o", {
|
|
674
|
+
chunk,
|
|
675
|
+
encoding
|
|
676
|
+
});
|
|
677
|
+
this.socket.emit("internal:write", chunk, encoding);
|
|
678
|
+
});
|
|
679
|
+
};
|
|
680
|
+
#onRealSocketConnect = () => {
|
|
681
|
+
if (!this.#passthroughSocket) return;
|
|
682
|
+
/**
|
|
683
|
+
* @note The consumer may destroy the socket while the passthrough
|
|
684
|
+
* connection is still being established. The passthrough "connect"
|
|
685
|
+
* (I/O poll phase) can arrive before the client's "close" callback
|
|
686
|
+
* (close phase) within the same event loop iteration. A destroyed
|
|
687
|
+
* socket must not emit "connect"/"ready".
|
|
688
|
+
*/
|
|
689
|
+
if (this.socket.destroyed) {
|
|
690
|
+
this.#passthroughSocket.destroy();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const replacedHandle = this.socket._handle;
|
|
694
|
+
const wasUnrefed = replacedHandle != null && typeof replacedHandle.hasRef === "function" && !replacedHandle.hasRef();
|
|
695
|
+
this.socket._handle = this.#passthroughSocket._handle;
|
|
696
|
+
this.#realHandleSwapped = true;
|
|
697
|
+
/**
|
|
698
|
+
* @note Preserve the ref state across the handle swap. If the
|
|
699
|
+
* consumer unrefed the socket while it was connecting, the swapped
|
|
700
|
+
* handle must not hold the process alive either.
|
|
701
|
+
*/
|
|
702
|
+
if (wasUnrefed) this.socket._handle.unref?.();
|
|
703
|
+
/**
|
|
704
|
+
* @note Close the replaced handle. Nothing references it past this
|
|
705
|
+
* point, and left open, it keeps the process alive indefinitely.
|
|
706
|
+
* For TLS sockets, the replaced handle is a TLSWrap; close its
|
|
707
|
+
* underlying transport too (closing the wrap alone does not close
|
|
708
|
+
* the TCP handle it sits on).
|
|
709
|
+
*/
|
|
710
|
+
if (replacedHandle != null) {
|
|
711
|
+
replacedHandle.close();
|
|
712
|
+
replacedHandle._parent?.close();
|
|
713
|
+
}
|
|
714
|
+
Reflect.set(this.socket, "connecting", false);
|
|
715
|
+
/**
|
|
716
|
+
* @note Read the remote address info once so it gets cached on the
|
|
717
|
+
* socket. The passthrough socket controls the swapped handle and may
|
|
718
|
+
* close it at any point (e.g. once the server ends the connection),
|
|
719
|
+
* while Node.js keeps serving the cached info for sockets that
|
|
720
|
+
* have connected. Reading must happen after the socket is no longer
|
|
721
|
+
* connecting (the info of connecting sockets is never cached).
|
|
722
|
+
*/
|
|
723
|
+
this.socket.remoteAddress;
|
|
724
|
+
this.socket.emit("connect");
|
|
725
|
+
this.socket.emit("ready");
|
|
726
|
+
};
|
|
727
|
+
#onRealSocketConnectionAttemptFailed = (address, port, family, error) => {
|
|
728
|
+
this.socket.emit("connectionAttemptFailed", address, port, family, error);
|
|
729
|
+
};
|
|
730
|
+
#onRealSocketConnectionAttemptTimeout = (address, port, family) => {
|
|
731
|
+
this.socket.emit("connectionAttemptTimeout", address, port, family);
|
|
732
|
+
};
|
|
733
|
+
#onRealSocketData = (data) => {
|
|
734
|
+
logger$1.verbose("real socket \"data\" event %o", data);
|
|
735
|
+
/**
|
|
736
|
+
* @note Receiving data is socket activity. Refresh the idle timer
|
|
737
|
+
* of the client socket the same way its own reads would
|
|
738
|
+
* ("socket.setTimeout()" must not fire during an active transfer).
|
|
739
|
+
* The data arrives on the real socket, which only refreshes the
|
|
740
|
+
* real socket's timer.
|
|
741
|
+
*/
|
|
742
|
+
this.socket._unrefTimer();
|
|
743
|
+
if (this.#readsCorked) {
|
|
744
|
+
logger$1.verbose("reads are corked, buffering the data...");
|
|
745
|
+
this.#corkedReads.push({
|
|
746
|
+
type: "data",
|
|
747
|
+
chunk: data
|
|
748
|
+
});
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (!this.socket.push(data)) {
|
|
752
|
+
logger$1.verbose("client socket forbade more pushes, pausing the passthrough socket...");
|
|
753
|
+
this.#passthroughSocket?.pause();
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
#onRealSocketError = (error) => {
|
|
757
|
+
logger$1.verbose("real socket \"error\" event %o", error);
|
|
758
|
+
if (this.socket.destroyed) {
|
|
759
|
+
logger$1.verbose("real socket errored but client socket already destroyed, skipping...");
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
logger$1.verbose("real socket errored, forwarding %o", error);
|
|
763
|
+
this.socket.destroy(error);
|
|
764
|
+
if (this.#realHandleSwapped) process.nextTick(() => this.socket.emit("close", true));
|
|
765
|
+
};
|
|
766
|
+
#onRealSocketEnd = () => {
|
|
767
|
+
this.socket._unrefTimer();
|
|
768
|
+
if (this.#readsCorked) {
|
|
769
|
+
this.#corkedReads.push({ type: "end" });
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
this.#clientEndPushed = true;
|
|
773
|
+
this.socket.push(null);
|
|
774
|
+
};
|
|
775
|
+
#onRealSocketClose = (hadError) => {
|
|
776
|
+
/**
|
|
777
|
+
* @note The connection is fully closed and the swapped handle
|
|
778
|
+
* cannot shut down anymore. Report a synchronous shutdown so the
|
|
779
|
+
* automatic "end()" of a half-closed socket ("allowHalfOpen: false")
|
|
780
|
+
* finishes without errors once the consumer reads the end-of-stream.
|
|
781
|
+
*/
|
|
782
|
+
if (this.#realHandleSwapped && this.socket._handle) this.socket._handle.shutdown = () => 1;
|
|
783
|
+
if (this.#readsCorked) {
|
|
784
|
+
this.#corkedReads.push({
|
|
785
|
+
type: "close",
|
|
786
|
+
hadError
|
|
787
|
+
});
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (this.#clientCloseEmitted) return;
|
|
791
|
+
if (this.socket.destroyed && !this.#realHandleSwapped) return;
|
|
792
|
+
this.#emitClientClose(hadError);
|
|
793
|
+
};
|
|
794
|
+
/**
|
|
795
|
+
* Emit the "close" event on the client socket, honoring the order
|
|
796
|
+
* of the socket teardown events.
|
|
797
|
+
* @note The client may be paused with the received data (and the
|
|
798
|
+
* end-of-stream) still buffered. Node.js never emits "close" before
|
|
799
|
+
* "end" on a gracefully closed connection: the teardown waits until
|
|
800
|
+
* the consumer reads the buffered data.
|
|
801
|
+
*/
|
|
802
|
+
#emitClientClose(hadError) {
|
|
803
|
+
if (this.#clientEndPushed && !this.socket.readableEnded && !this.socket.destroyed) {
|
|
804
|
+
let closeDelivered = false;
|
|
805
|
+
const deliverClose = (hadErrorOverride) => {
|
|
806
|
+
if (closeDelivered) return;
|
|
807
|
+
closeDelivered = true;
|
|
808
|
+
process.nextTick(() => {
|
|
809
|
+
if (!this.#clientCloseEmitted) this.socket.emit("close", hadErrorOverride ?? hadError);
|
|
810
|
+
});
|
|
811
|
+
};
|
|
812
|
+
this.socket.once("end", () => {
|
|
813
|
+
deliverClose();
|
|
814
|
+
});
|
|
815
|
+
/**
|
|
816
|
+
* @note The consumer may also destroy the socket before reading
|
|
817
|
+
* the buffered data. Node.js still emits "close" for such
|
|
818
|
+
* sockets, but the destroy machinery of a socket with a swapped
|
|
819
|
+
* (already closed) handle never completes. Deliver "close" here.
|
|
820
|
+
*/
|
|
821
|
+
const realDestroy = this.socket._destroy;
|
|
822
|
+
this.socket._destroy = (error, callback) => {
|
|
823
|
+
deliverClose(error != null);
|
|
824
|
+
return realDestroy.call(this.socket, error, callback);
|
|
825
|
+
};
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
this.socket.emit("close", hadError);
|
|
829
|
+
}
|
|
830
|
+
#onMockSocketDrain = () => {
|
|
831
|
+
logger$1.verbose("client socket drained!");
|
|
832
|
+
this.#passthroughSocket?.resume();
|
|
833
|
+
};
|
|
834
|
+
/**
|
|
835
|
+
* Suspend forwarding of the passthrough socket events ("data", "end", "close")
|
|
836
|
+
* to the client socket. The events are buffered in order until `uncorkReads()`
|
|
837
|
+
* is called. This allows the consumer to delay the delivery of the original
|
|
838
|
+
* response to the client (e.g. until its own asynchronous logic settles).
|
|
839
|
+
*
|
|
840
|
+
* @note Pausing the client socket is not enough to delay the delivery.
|
|
841
|
+
* Consumers like Undici read the pushed data from a paused socket
|
|
842
|
+
* directly via `socket.read()`, which is unaffected by `socket.pause()`.
|
|
843
|
+
*/
|
|
844
|
+
corkReads() {
|
|
845
|
+
this.#readsCorked = true;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Resume forwarding of the passthrough socket events to the client socket,
|
|
849
|
+
* replaying any events buffered while the reads were corked.
|
|
850
|
+
*/
|
|
851
|
+
uncorkReads() {
|
|
852
|
+
if (!this.#readsCorked) return;
|
|
853
|
+
this.#readsCorked = false;
|
|
854
|
+
for (const corkedRead of this.#corkedReads.splice(0)) switch (corkedRead.type) {
|
|
855
|
+
case "data":
|
|
856
|
+
if (!this.socket.push(corkedRead.chunk)) {
|
|
857
|
+
logger$1.verbose("client socket forbade more pushes, pausing the passthrough socket...");
|
|
858
|
+
this.#passthroughSocket?.pause();
|
|
859
|
+
}
|
|
860
|
+
break;
|
|
861
|
+
case "end":
|
|
862
|
+
this.#clientEndPushed = true;
|
|
863
|
+
this.socket.push(null);
|
|
864
|
+
break;
|
|
865
|
+
case "close":
|
|
866
|
+
this.#emitClientClose(corkedRead.hadError);
|
|
867
|
+
break;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
claim() {
|
|
871
|
+
super.claim();
|
|
872
|
+
/**
|
|
873
|
+
* @note The client may destroy the socket before the connection
|
|
874
|
+
* is claimed (e.g. abort a request in-flight). There is no
|
|
875
|
+
* connection to mock then, and the destroyed socket has no handle.
|
|
876
|
+
*/
|
|
877
|
+
if (this.socket.destroyed) {
|
|
878
|
+
logger$1.verbose("socket already destroyed, skipping claim...");
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* @note Skip already connected sockets (e.g. kept-alive sockets
|
|
883
|
+
* reused for the next exchange). Sockets with an emulated "connect"
|
|
884
|
+
* only appear connected and must still complete the mock connection.
|
|
885
|
+
*/
|
|
886
|
+
if (!this.socket.connecting && !this.#connectEmulated) {
|
|
887
|
+
logger$1.verbose("socket already connected, skipping claim...");
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
logger$1.verbose("-> claim!");
|
|
891
|
+
/**
|
|
892
|
+
* @note Reflect the local end of the claimed socket, the same way
|
|
893
|
+
* the operating system reports the bound address of an outgoing
|
|
894
|
+
* connection via "socket.address()"/"localAddress"/"localPort".
|
|
895
|
+
* Patching the handle also prevents Node.js from handling the
|
|
896
|
+
* "getsockname" errors of the never-connected raw handle.
|
|
897
|
+
* @see https://github.com/nodejs/node/blob/13eb80f3b718452213e0fc449702aefbbfe4110f/lib/net.js#L971
|
|
898
|
+
*/
|
|
899
|
+
this.socket._handle.getsockname = (addressInfo) => {
|
|
900
|
+
Object.assign(addressInfo, getLocalAddressInfoByConnectionOptions(this.#connectionOptions));
|
|
901
|
+
return 0;
|
|
902
|
+
};
|
|
903
|
+
/**
|
|
904
|
+
* @note Reflect the connection target as the peer of the claimed
|
|
905
|
+
* socket, the same way a connected socket reports the server it
|
|
906
|
+
* connected to via "remoteAddress"/"remotePort"/"remoteFamily".
|
|
907
|
+
*/
|
|
908
|
+
this.socket._handle.getpeername = (addressInfo) => {
|
|
909
|
+
Object.assign(addressInfo, getAddressInfoByConnectionOptions(this.#connectionOptions));
|
|
910
|
+
return 0;
|
|
911
|
+
};
|
|
912
|
+
this.#bufferedWrites = [];
|
|
913
|
+
this.socket._pendingData = null;
|
|
914
|
+
this.socket._pendingEncoding = "";
|
|
915
|
+
this.pendingConnection.promise.then(([request, handle]) => {
|
|
916
|
+
logger$1.verbose("connection request resolved, mocking the connection...");
|
|
917
|
+
/**
|
|
918
|
+
* @note "afterConnect" asserts that the socket is connecting.
|
|
919
|
+
* Restore the flag if the connect was emulated earlier (emulation
|
|
920
|
+
* flips it so its listeners observe a connected socket).
|
|
921
|
+
* "afterConnect" itself sets it back to false.
|
|
922
|
+
*/
|
|
923
|
+
if (this.#connectEmulated) Reflect.set(this.socket, "connecting", true);
|
|
924
|
+
/**
|
|
925
|
+
* @see https://github.com/nodejs/node/blob/9cd6630870b776e96c5cf0ac68c31e2f46df3835/lib/net.js#L1142
|
|
926
|
+
*/
|
|
927
|
+
request.oncomplete(0, handle, request, true, true);
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
passthrough(flushPendingData) {
|
|
931
|
+
super.passthrough();
|
|
932
|
+
logger$1.verbose("-> passthrough!");
|
|
933
|
+
const createRealSocket = () => {
|
|
934
|
+
const realSocket = this.createConnection();
|
|
935
|
+
realSocket[kPatched] = true;
|
|
936
|
+
if (this.socket.timeout != null) realSocket.setTimeout(this.socket.timeout);
|
|
937
|
+
return realSocket;
|
|
938
|
+
};
|
|
939
|
+
const realSocket = this.#passthroughSocket && !this.#passthroughSocket.destroyed ? this.#passthroughSocket : createRealSocket();
|
|
940
|
+
if (realSocket !== this.#passthroughSocket) this.#passthroughSocket = realSocket;
|
|
941
|
+
if (this.#bufferedWrites.length === 0) logger$1.verbose("passthrough with empty writes buffer (state: %d)", this.readyState);
|
|
942
|
+
/**
|
|
943
|
+
* Flush any writes during the pending phase to the passthrough socket.
|
|
944
|
+
* @note These are written directly on the passthrough socket to prevent
|
|
945
|
+
* them from being forwarded as "data" events on the server (already emitted).
|
|
946
|
+
*/
|
|
947
|
+
for (let i = 0; i < this.#bufferedWrites.length; i++) {
|
|
948
|
+
const pendingWrite = this.#bufferedWrites[i];
|
|
949
|
+
if (i === 0 && typeof flushPendingData === "function") {
|
|
950
|
+
const data = pendingWrite[1];
|
|
951
|
+
const encoding = pendingWrite[2];
|
|
952
|
+
flushPendingData(data, encoding, (nextData) => {
|
|
953
|
+
pendingWrite[1] = nextData;
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
const [, data, encoding, callback] = pendingWrite;
|
|
957
|
+
writePendingData(realSocket, data, encoding, callback);
|
|
958
|
+
}
|
|
959
|
+
this.#bufferedWrites = [];
|
|
960
|
+
this.socket._pendingData = null;
|
|
961
|
+
this.socket._pendingEncoding = "";
|
|
962
|
+
this.socket.address = realSocket.address.bind(realSocket);
|
|
963
|
+
this.socket.removeListener("drain", this.#onMockSocketDrain);
|
|
964
|
+
this.socket.on("drain", this.#onMockSocketDrain);
|
|
965
|
+
realSocket.removeListener("connect", this.#onRealSocketConnect).removeListener("connectionAttemptFailed", this.#onRealSocketConnectionAttemptFailed).removeListener("connectionAttemptTimeout", this.#onRealSocketConnectionAttemptTimeout).removeListener("data", this.#onRealSocketData).removeListener("error", this.#onRealSocketError).removeListener("end", this.#onRealSocketEnd).removeListener("close", this.#onRealSocketClose);
|
|
966
|
+
realSocket.once("connect", this.#onRealSocketConnect).on("connectionAttemptFailed", this.#onRealSocketConnectionAttemptFailed).on("connectionAttemptTimeout", this.#onRealSocketConnectionAttemptTimeout).on("data", this.#onRealSocketData).on("error", this.#onRealSocketError).on("end", this.#onRealSocketEnd).on("close", this.#onRealSocketClose);
|
|
967
|
+
return realSocket;
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
var TlsSocketController = class extends TcpSocketController {
|
|
971
|
+
/**
|
|
972
|
+
* @note The TLS connection options must be provided explicitly.
|
|
973
|
+
* They cannot be captured from "socket.connect()" like for plain
|
|
974
|
+
* TCP sockets because "tls.connect()" fixes them at the TLS socket
|
|
975
|
+
* construction, before its transport ever connects.
|
|
976
|
+
*/
|
|
977
|
+
#tlsConnectionOptions;
|
|
978
|
+
constructor(socket, createConnection, tlsConnectionOptions) {
|
|
979
|
+
super(socket, createConnection, tlsConnectionOptions);
|
|
980
|
+
this.socket = socket;
|
|
981
|
+
this.createConnection = createConnection;
|
|
982
|
+
this.#tlsConnectionOptions = tlsConnectionOptions;
|
|
983
|
+
socket.prependListener("secureConnect", () => {
|
|
984
|
+
/**
|
|
985
|
+
* @note Reflect the negotiated ALPN protocol from the handle that
|
|
986
|
+
* completed the handshake. The socket sets "alpnProtocol" only in
|
|
987
|
+
* its own "_finishInit", which never runs for passthrough
|
|
988
|
+
* connections (the handshake completes on the passthrough socket
|
|
989
|
+
* whose handle this socket inherits).
|
|
990
|
+
*/
|
|
991
|
+
socket.alpnProtocol = socket._handle.getALPNNegotiatedProtocol();
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
emulateConnect() {
|
|
995
|
+
super.emulateConnect();
|
|
996
|
+
for (const listener of this.socket.rawListeners("secureConnect")) listener.apply(this.socket);
|
|
997
|
+
}
|
|
998
|
+
claim() {
|
|
999
|
+
/**
|
|
1000
|
+
* @note The client may destroy the socket before the connection
|
|
1001
|
+
* is claimed. Defer to the parent class, which transitions the
|
|
1002
|
+
* ready state and skips the mock connection of destroyed sockets.
|
|
1003
|
+
*/
|
|
1004
|
+
if (this.socket.destroyed) {
|
|
1005
|
+
super.claim();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* @note Reflect that the mocked connection is not authorized.
|
|
1010
|
+
* There is no real peer certificate to verify. The identity check
|
|
1011
|
+
* itself is skipped for claimed connections (see the
|
|
1012
|
+
* "isSessionReused" mock below).
|
|
1013
|
+
*/
|
|
1014
|
+
this.socket.prependOnceListener("secureConnect", () => {
|
|
1015
|
+
Reflect.set(this.socket, "authorized", false);
|
|
1016
|
+
Reflect.set(this.socket, "authorizationError", "MOCKED_CONNECTION_NOT_VERIFIED");
|
|
1017
|
+
});
|
|
1018
|
+
const handle = this.socket._handle;
|
|
1019
|
+
handle.start = () => void 0;
|
|
1020
|
+
/**
|
|
1021
|
+
* Mock this to prevent the "Error: Worker exited unexpectedly" error.
|
|
1022
|
+
* This will trigger when "secure" is emitted.
|
|
1023
|
+
* @see https://github.com/nodejs/node/blob/bdc8131fa78089b81b74dbff467365afb6536e6a/lib/internal/tls/wrap.js#L1648
|
|
1024
|
+
*/
|
|
1025
|
+
handle.verifyError = () => void 0;
|
|
1026
|
+
handle.getSession = () => {
|
|
1027
|
+
return Buffer.from("mocked session");
|
|
1028
|
+
};
|
|
1029
|
+
/**
|
|
1030
|
+
* @note Skip the server identity check for this mocked connection.
|
|
1031
|
+
* Node.js runs "options.checkServerIdentity" in "onConnectSecure"
|
|
1032
|
+
* against the peer certificate, and a mocked connection has no
|
|
1033
|
+
* real peer certificate — the caller's (or the default) validation
|
|
1034
|
+
* would fail and destroy the socket. The check is overridden on the
|
|
1035
|
+
* socket's internal connect options since the emulated handshake
|
|
1036
|
+
* still completes through the regular "onConnectSecure" path.
|
|
1037
|
+
* @see https://github.com/nodejs/node/blob/3178a762d6a2b1a37b74f02266eea0f3d86603f1/lib/_tls_wrap.js#L1621
|
|
1038
|
+
*/
|
|
1039
|
+
const realTlsConnectOptions = getTlsConnectOptions(this.socket);
|
|
1040
|
+
if (realTlsConnectOptions) realTlsConnectOptions.checkServerIdentity = () => {};
|
|
1041
|
+
handle.getCipher = () => {
|
|
1042
|
+
return {
|
|
1043
|
+
name: "TLS_AES_256_GCM_SHA384",
|
|
1044
|
+
standardName: "TLS_AES_256_GCM_SHA384",
|
|
1045
|
+
version: "TLSv1.3"
|
|
1046
|
+
};
|
|
1047
|
+
};
|
|
1048
|
+
/**
|
|
1049
|
+
* Mock this to prevent a segfault on Node.js 26+. The native
|
|
1050
|
+
* implementation reads the negotiated group ("SSL_get0_group_name")
|
|
1051
|
+
* of a handshake that never happened. Node.js itself calls this
|
|
1052
|
+
* in "onConnectSecure" to validate the "minDHSize" option.
|
|
1053
|
+
* Reflect the ephemeral key exchange matching the mocked cipher.
|
|
1054
|
+
* @see https://github.com/nodejs/node/blob/3178a762d6a2b1a37b74f02266eea0f3d86603f1/lib/_tls_wrap.js#L1636
|
|
1055
|
+
*/
|
|
1056
|
+
handle.getEphemeralKeyInfo = () => {
|
|
1057
|
+
return {
|
|
1058
|
+
type: "ECDH",
|
|
1059
|
+
name: "X25519",
|
|
1060
|
+
size: 253
|
|
1061
|
+
};
|
|
1062
|
+
};
|
|
1063
|
+
const requestedAlpnProtocols = this.#tlsConnectionOptions?.ALPNProtocols;
|
|
1064
|
+
if (Array.isArray(requestedAlpnProtocols) && requestedAlpnProtocols.length > 0) {
|
|
1065
|
+
const [preferredProtocol] = requestedAlpnProtocols;
|
|
1066
|
+
/**
|
|
1067
|
+
* @note Reflect the client's preferred ALPN protocol as the
|
|
1068
|
+
* negotiated one. The mocked server accepts whatever the
|
|
1069
|
+
* client prefers.
|
|
1070
|
+
*/
|
|
1071
|
+
handle.getALPNNegotiatedProtocol = () => {
|
|
1072
|
+
return typeof preferredProtocol === "string" ? preferredProtocol : false;
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
this.socket.once("connect", () => {
|
|
1076
|
+
/**
|
|
1077
|
+
* @note A TLS 1.3 handshake derives five secrets, each reported
|
|
1078
|
+
* via a separate "keylog" event before the handshake completes.
|
|
1079
|
+
* Reflect them with mocked key material matching the mocked
|
|
1080
|
+
* cipher (SHA-384 secrets; the format is "LABEL <client random>
|
|
1081
|
+
* <secret>", each value hex-encoded).
|
|
1082
|
+
*/
|
|
1083
|
+
const mockedClientRandom = "0".repeat(64);
|
|
1084
|
+
const mockedSecret = "0".repeat(96);
|
|
1085
|
+
for (const keylogLabel of [
|
|
1086
|
+
"SERVER_HANDSHAKE_TRAFFIC_SECRET",
|
|
1087
|
+
"EXPORTER_SECRET",
|
|
1088
|
+
"SERVER_TRAFFIC_SECRET_0",
|
|
1089
|
+
"CLIENT_HANDSHAKE_TRAFFIC_SECRET",
|
|
1090
|
+
"CLIENT_TRAFFIC_SECRET_0"
|
|
1091
|
+
]) this.socket.emit("keylog", Buffer.from(`${keylogLabel} ${mockedClientRandom} ${mockedSecret}\n`));
|
|
1092
|
+
handle.onhandshakedone();
|
|
1093
|
+
/**
|
|
1094
|
+
* @note A TLS 1.3 server issues two session tickets by default,
|
|
1095
|
+
* each emitting a separate "session" event on the client.
|
|
1096
|
+
*/
|
|
1097
|
+
handle.onnewsession(1, Buffer.from("mocked session"));
|
|
1098
|
+
handle.onnewsession(2, Buffer.from("mocked session"));
|
|
1099
|
+
});
|
|
1100
|
+
super.claim();
|
|
1101
|
+
}
|
|
1102
|
+
passthrough(flushPendingData) {
|
|
1103
|
+
const realSocket = super.passthrough(flushPendingData);
|
|
1104
|
+
/**
|
|
1105
|
+
* @note Remove the internal "connect" listener added by the TLS socket.
|
|
1106
|
+
* Normally, that listener manages the SSL handshake. But since we're in passthrough,
|
|
1107
|
+
* we delegate that to the real socket. Leaving the listener on the mock socket while
|
|
1108
|
+
* inheriting the real socket's handle will result in the handshake performed twice, which is a no-op.
|
|
1109
|
+
* @see https://github.com/nodejs/node/blob/abddfc921bf2af02a04a6a5d2bca8e2d91d80958/lib/internal/tls/wrap.js#L1105
|
|
1110
|
+
*
|
|
1111
|
+
* This prevents the following error:
|
|
1112
|
+
* # node (vitest 4)[8686]: static void node::crypto::TLSWrap::Start(const FunctionCallbackInfo<Value> &) at ../src/crypto/crypto_tls.cc:589
|
|
1113
|
+
# Assertion failed: !wrap->started_
|
|
1114
|
+
*/
|
|
1115
|
+
for (const connectListener of this.socket.listeners("connect")) if (connectListener === this.socket._start || "listener" in connectListener && connectListener.listener === this.socket._start) this.socket.removeListener("connect", connectListener);
|
|
1116
|
+
realSocket.on("secure", () => {
|
|
1117
|
+
this.socket.emit("secure");
|
|
1118
|
+
}).on("session", (...args) => {
|
|
1119
|
+
this.socket.emit("session", ...args);
|
|
1120
|
+
}).on("keylog", (...args) => {
|
|
1121
|
+
this.socket.emit("keylog", ...args);
|
|
1122
|
+
}).on("OCSPResponse", (...args) => {
|
|
1123
|
+
this.socket.emit("OCSPResponse", ...args);
|
|
1124
|
+
});
|
|
1125
|
+
return realSocket;
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
//#endregion
|
|
1129
|
+
//#region src/interceptors/net/utils/normalize-tls-connect-args.ts
|
|
1130
|
+
function normalizeTlsConnectArgs(args) {
|
|
1131
|
+
/**
|
|
1132
|
+
* @note Despite incorrect type definitions, "tls.connect()" has all the
|
|
1133
|
+
* options of "net.connect()" and then those specific to TLS connections,
|
|
1134
|
+
* like "session" or "socket".
|
|
1135
|
+
* @see https://github.com/nodejs/node/blob/bdc8131fa78089b81b74dbff467365afb6536e6a/lib/internal/tls/wrap.js#L1615
|
|
1136
|
+
*/
|
|
1137
|
+
const netConnectArgs = normalizeNetConnectArgs(args);
|
|
1138
|
+
const options = netConnectArgs[0];
|
|
1139
|
+
const callback = netConnectArgs[1];
|
|
1140
|
+
if (args[0] !== null && typeof args[0] === "object") Object.assign(options, args[0]);
|
|
1141
|
+
else if (args[1] !== null && typeof args[1] === "object") Object.assign(options, args[1]);
|
|
1142
|
+
else if (args[2] !== null && typeof args[2] === "object") Object.assign(options, args[2]);
|
|
1143
|
+
return callback ? [options, callback] : [options];
|
|
1144
|
+
}
|
|
1145
|
+
//#endregion
|
|
1146
|
+
//#region src/interceptors/net/index.ts
|
|
1147
|
+
var SocketConnectionEvent = class extends TypedEvent {
|
|
1148
|
+
constructor(data) {
|
|
1149
|
+
super(...["connection", {}]);
|
|
1150
|
+
this.socket = data.socket;
|
|
1151
|
+
this.connectionOptions = data.connectionOptions;
|
|
1152
|
+
this.controller = data.controller;
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
const logger = createLogger("socket");
|
|
1156
|
+
/**
|
|
1157
|
+
* A DNS lookup function for intercepted sockets. It always succeeds,
|
|
1158
|
+
* resolving any hostname to the loopback address. This ensures the
|
|
1159
|
+
* "lookup"/"connectionAttempt" socket events fire even for non-existent
|
|
1160
|
+
* hosts, and no real DNS resolution is performed. Passthrough
|
|
1161
|
+
* connections are created with the original options and use the real
|
|
1162
|
+
* (or the caller's custom) lookup instead.
|
|
1163
|
+
*/
|
|
1164
|
+
const mockLookup = (hostname, dnsOptions, callback) => {
|
|
1165
|
+
const family = dnsOptions.family === 6 ? 6 : 4;
|
|
1166
|
+
const address = family === 6 ? "::1" : "127.0.0.1";
|
|
1167
|
+
/**
|
|
1168
|
+
* @note Call back asynchronously since DNS lookup is always
|
|
1169
|
+
* asynchronous in Node.js. Calling back synchronously emits
|
|
1170
|
+
* the "lookup"/"connectionAttempt" socket events before the
|
|
1171
|
+
* consumer gets a chance to add listeners for them.
|
|
1172
|
+
*/
|
|
1173
|
+
process.nextTick(() => {
|
|
1174
|
+
/**
|
|
1175
|
+
* @note Honor the Node.js lookup contract: the callback receives
|
|
1176
|
+
* an array of addresses only when the "all" option is set
|
|
1177
|
+
* (e.g. during the family autoselection). Otherwise, it receives
|
|
1178
|
+
* a single address and its family. Node.js rejects an array in
|
|
1179
|
+
* the latter case with "ERR_INVALID_IP_ADDRESS".
|
|
1180
|
+
*/
|
|
1181
|
+
if (dnsOptions.all) {
|
|
1182
|
+
callback(null, [{
|
|
1183
|
+
address,
|
|
1184
|
+
family
|
|
1185
|
+
}]);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
callback(null, address, family);
|
|
1189
|
+
});
|
|
1190
|
+
};
|
|
1191
|
+
/**
|
|
1192
|
+
* Interceptor for `net.Socket` connections.
|
|
1193
|
+
*/
|
|
1194
|
+
var SocketInterceptor = class extends Interceptor {
|
|
1195
|
+
static {
|
|
1196
|
+
this.symbol = Symbol.for("socket-interceptor");
|
|
1197
|
+
}
|
|
1198
|
+
predicate() {
|
|
1199
|
+
return true;
|
|
1200
|
+
}
|
|
1201
|
+
setup() {
|
|
1202
|
+
const interceptor = this;
|
|
1203
|
+
/**
|
|
1204
|
+
* @note A synchronous re-entrancy latch for creating passthrough
|
|
1205
|
+
* TLS connections. The original "tls.connect()" constructs a TLS
|
|
1206
|
+
* socket and calls "socket.connect()" on it synchronously, and that
|
|
1207
|
+
* call must reach Node.js as-is instead of being intercepted again.
|
|
1208
|
+
*/
|
|
1209
|
+
let isCreatingPassthroughConnection = false;
|
|
1210
|
+
this.subscriptions.push(
|
|
1211
|
+
/**
|
|
1212
|
+
* @note Intercept connections at the "net.Socket.prototype.connect"
|
|
1213
|
+
* level instead of patching the "net.connect()" module function.
|
|
1214
|
+
* ESM consumers snapshot the module bindings at import time
|
|
1215
|
+
* ("import * as net from 'node:net'"), so reassigning "net.connect"
|
|
1216
|
+
* is invisible to them. Every client connection ends up calling
|
|
1217
|
+
* "Socket.prototype.connect" (including the one made by the original
|
|
1218
|
+
* "net.connect()"), and prototype mutations are visible regardless
|
|
1219
|
+
* of how the module was imported.
|
|
1220
|
+
*/
|
|
1221
|
+
patchesRegistry.applyPatch(net.Socket.prototype, "connect", (realSocketConnect) => {
|
|
1222
|
+
return function connect(...args) {
|
|
1223
|
+
const socket = this;
|
|
1224
|
+
/**
|
|
1225
|
+
* @note Skip the sockets this interceptor already controls
|
|
1226
|
+
* (the mock connect call below, re-connects of an intercepted
|
|
1227
|
+
* socket, passthrough sockets) and the sockets created while
|
|
1228
|
+
* dialing a passthrough TLS connection. Their connects must
|
|
1229
|
+
* reach Node.js as-is.
|
|
1230
|
+
*/
|
|
1231
|
+
if (socket[kPatched] || isCreatingPassthroughConnection) return realSocketConnect.apply(socket, args);
|
|
1232
|
+
logger.verbose("socket.connect() %o", args);
|
|
1233
|
+
/**
|
|
1234
|
+
* @note The original "net.connect()"/"tls.connect()" normalize
|
|
1235
|
+
* the arguments themselves and call this method with the
|
|
1236
|
+
* normalized array instead of the individual arguments.
|
|
1237
|
+
*/
|
|
1238
|
+
const connectArgs = Array.isArray(args[0]) ? args[0] : args;
|
|
1239
|
+
const [transportConnectionOptions, connectionCallback] = normalizeNetConnectArgs(connectArgs);
|
|
1240
|
+
logger.verbose("connection options %o", {
|
|
1241
|
+
transportConnectionOptions,
|
|
1242
|
+
connectionCallback
|
|
1243
|
+
});
|
|
1244
|
+
let controller;
|
|
1245
|
+
let connectionOptions;
|
|
1246
|
+
if (socket instanceof tls.TLSSocket) {
|
|
1247
|
+
/**
|
|
1248
|
+
* @note TLS sockets arrive here from the original
|
|
1249
|
+
* "tls.connect()", which connects the transport of the TLS
|
|
1250
|
+
* socket it constructs ("connectionCallback" is its internal
|
|
1251
|
+
* "_start" handshake trigger). The original TLS connection
|
|
1252
|
+
* options are recovered from the socket instance since the
|
|
1253
|
+
* construction has already happened.
|
|
1254
|
+
*/
|
|
1255
|
+
const realTlsConnectionOptions = getTlsConnectOptions(socket);
|
|
1256
|
+
const [tlsConnectionOptions] = normalizeTlsConnectArgs([{
|
|
1257
|
+
...transportConnectionOptions,
|
|
1258
|
+
...realTlsConnectionOptions
|
|
1259
|
+
}]);
|
|
1260
|
+
connectionOptions = tlsConnectionOptions;
|
|
1261
|
+
controller = new TlsSocketController(socket, () => {
|
|
1262
|
+
/**
|
|
1263
|
+
* @note Create the passthrough connection via the original
|
|
1264
|
+
* "tls.connect()" with the original connection options
|
|
1265
|
+
* (the real DNS lookup and the caller's certificate
|
|
1266
|
+
* validation included). The latch exempts the transport
|
|
1267
|
+
* connect of that connection from interception.
|
|
1268
|
+
*/
|
|
1269
|
+
isCreatingPassthroughConnection = true;
|
|
1270
|
+
try {
|
|
1271
|
+
return tls.connect(realTlsConnectionOptions ?? tlsConnectionOptions);
|
|
1272
|
+
} finally {
|
|
1273
|
+
isCreatingPassthroughConnection = false;
|
|
1274
|
+
}
|
|
1275
|
+
}, tlsConnectionOptions);
|
|
1276
|
+
} else {
|
|
1277
|
+
/**
|
|
1278
|
+
* @note Create passthrough connections without the connection
|
|
1279
|
+
* callback. The callback is already registered as a "connect"
|
|
1280
|
+
* listener on the consumer's socket. Passing it to the
|
|
1281
|
+
* passthrough connection would invoke it twice.
|
|
1282
|
+
*/
|
|
1283
|
+
const passthroughArgs = connectArgs.filter((arg) => {
|
|
1284
|
+
return typeof arg !== "function";
|
|
1285
|
+
});
|
|
1286
|
+
/**
|
|
1287
|
+
* @note Create the passthrough socket with the original
|
|
1288
|
+
* options, the same way "net.connect()" does. Connect it via
|
|
1289
|
+
* the unpatched method so the passthrough connection is not
|
|
1290
|
+
* intercepted again.
|
|
1291
|
+
*/
|
|
1292
|
+
const socketOptions = connectArgs[0] !== null && typeof connectArgs[0] === "object" && !("href" in connectArgs[0]) ? connectArgs[0] : {};
|
|
1293
|
+
connectionOptions = transportConnectionOptions;
|
|
1294
|
+
controller = new TcpSocketController(socket, () => {
|
|
1295
|
+
const passthroughSocket = new net.Socket(socketOptions);
|
|
1296
|
+
Reflect.apply(realSocketConnect, passthroughSocket, passthroughArgs);
|
|
1297
|
+
return passthroughSocket;
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
process.nextTick(() => {
|
|
1301
|
+
if (socket.destroyed) return;
|
|
1302
|
+
if (!interceptor.emitter.emit(new SocketConnectionEvent({
|
|
1303
|
+
socket: controller.serverSocket,
|
|
1304
|
+
controller,
|
|
1305
|
+
connectionOptions
|
|
1306
|
+
}))) {
|
|
1307
|
+
logger.verbose("no \"connection\" listeners found on the interceptor, passthrough...");
|
|
1308
|
+
controller.passthrough();
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
logger.verbose("emitted \"connection\" event!");
|
|
1312
|
+
});
|
|
1313
|
+
logger.verbose("connecting the socket...");
|
|
1314
|
+
/**
|
|
1315
|
+
* @note The requested local address/port are stripped from the
|
|
1316
|
+
* actual "socket.connect()" call by the controller to prevent
|
|
1317
|
+
* binding the intercepted socket (see the "connect" proxy).
|
|
1318
|
+
*/
|
|
1319
|
+
const mockConnectionOptions = { ...transportConnectionOptions };
|
|
1320
|
+
mockConnectionOptions.lookup = mockLookup;
|
|
1321
|
+
try {
|
|
1322
|
+
/**
|
|
1323
|
+
* @note The normalized options are looser than the declared
|
|
1324
|
+
* "SocketConnectOpts" (e.g. the port may be a string when a
|
|
1325
|
+
* URL is passed). Node.js validates them at runtime.
|
|
1326
|
+
* This call goes through the controller's "connect" proxy
|
|
1327
|
+
* and lands back in this patch, where the "kPatched" check
|
|
1328
|
+
* above delegates it to the unpatched method.
|
|
1329
|
+
*/
|
|
1330
|
+
return socket.connect(mockConnectionOptions, connectionCallback ?? void 0);
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
/**
|
|
1333
|
+
* @note "socket.connect()" can throw synchronously on invalid
|
|
1334
|
+
* input (e.g. a bad port). Destroy the socket so the pending
|
|
1335
|
+
* interception tick does not act on it, then let the error
|
|
1336
|
+
* propagate to the consumer like in Node.js.
|
|
1337
|
+
*/
|
|
1338
|
+
socket.destroy();
|
|
1339
|
+
throw error;
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
}),
|
|
1343
|
+
this.#stopReusingUnpatchedSockets()
|
|
1344
|
+
);
|
|
1345
|
+
/**
|
|
1346
|
+
* @note "net.connect()"/"net.createConnection()" need no patching of
|
|
1347
|
+
* their own: both construct a "net.Socket" and call the patched
|
|
1348
|
+
* "Socket.prototype.connect" on it.
|
|
1349
|
+
*/
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Prevent the `net.Socket` instances created before this interceptor
|
|
1353
|
+
* was applied from getting reused by an `Agent`. Purging them from the
|
|
1354
|
+
* keep-alive pool forces the agent to establish new (intercepted)
|
|
1355
|
+
* connections instead.
|
|
1356
|
+
*/
|
|
1357
|
+
#stopReusingUnpatchedSockets() {
|
|
1358
|
+
/**
|
|
1359
|
+
* @note "Agent.prototype.addRequest" is undocumented but stable:
|
|
1360
|
+
* its signature changed once (the keep-alive Agent rewrite in
|
|
1361
|
+
* Node.js 0.12), and the ecosystem (agent-base, agentkeepalive,
|
|
1362
|
+
* APM wrappers) has relied on it ever since. "https.Agent"
|
|
1363
|
+
* inherits it, so a single patch covers both.
|
|
1364
|
+
*/
|
|
1365
|
+
if (typeof http.Agent.prototype.addRequest !== "function") return () => {};
|
|
1366
|
+
return patchesRegistry.applyPatch(http.Agent.prototype, "addRequest", (realAddRequest) => {
|
|
1367
|
+
return function(...args) {
|
|
1368
|
+
/**
|
|
1369
|
+
* @note Destroy the free sockets created before the interceptor
|
|
1370
|
+
* was applied. Destroying flips their "destroyed" state
|
|
1371
|
+
* synchronously, so the original "addRequest" below discards
|
|
1372
|
+
* them and dials a new (intercepted) connection instead.
|
|
1373
|
+
* The "freeSockets" pool only contains idle sockets by
|
|
1374
|
+
* definition, so destroying them aborts nothing in-flight.
|
|
1375
|
+
*/
|
|
1376
|
+
for (const sockets of Object.values(this.freeSockets)) {
|
|
1377
|
+
if (sockets == null) continue;
|
|
1378
|
+
for (const socket of sockets) if (!socket[kPatched]) socket.destroy();
|
|
1379
|
+
}
|
|
1380
|
+
return realAddRequest?.apply(this, args);
|
|
1381
|
+
};
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
//#endregion
|
|
1386
|
+
export { getDeepPropertyDescriptor as a, unwrapPendingData as i, SocketController as n, patchesRegistry as o, kRawSocket as r, SocketInterceptor as t };
|
|
1387
|
+
|
|
1388
|
+
//# sourceMappingURL=net-DtMnyEeg.js.map
|