harper 5.1.20 → 5.2.0-alpha.5
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/agent/operations.ts +13 -16
- package/components/Application.ts +188 -7
- package/components/ApplicationScope.ts +3 -0
- package/components/Scope.ts +12 -0
- package/components/anthropic/index.ts +9 -2
- package/components/bedrock/index.ts +25 -2
- package/components/componentLoader.ts +56 -7
- package/components/componentSecrets.ts +451 -0
- package/components/deploymentRecorder.ts +7 -1
- package/components/mcp/adapters/fastify.ts +3 -0
- package/components/mcp/adapters/harperHttp.ts +3 -0
- package/components/mcp/audit.ts +21 -6
- package/components/mcp/customResourceRegistry.ts +17 -38
- package/components/mcp/listChanged.ts +37 -20
- package/components/mcp/quota.ts +145 -0
- package/components/mcp/rateLimit.ts +152 -14
- package/components/mcp/resources.ts +2 -4
- package/components/mcp/toolRegistry.ts +73 -4
- package/components/mcp/tools/application.ts +77 -8
- package/components/mcp/tools/operations.ts +106 -36
- package/components/mcp/tools/schemas/operationDescriptions.ts +4 -4
- package/components/mcp/tools/schemas/operations.ts +5 -0
- package/components/mcp/transport.ts +55 -4
- package/components/ollama/index.ts +6 -2
- package/components/openai/index.ts +9 -2
- package/components/operations.js +204 -10
- package/components/operationsValidation.js +173 -0
- package/components/secretOperations.ts +498 -0
- package/config/configUtils.ts +25 -1
- package/config-app.schema.json +81 -0
- package/config-root.schema.json +8 -1
- package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -3
- package/dataLayer/readAuditLog.ts +14 -0
- package/dataLayer/schemaDescribe.ts +1 -0
- package/dist/agent/operations.d.ts +7 -4
- package/dist/agent/operations.js +13 -15
- package/dist/agent/operations.js.map +1 -1
- package/dist/components/Application.d.ts +25 -2
- package/dist/components/Application.js +166 -7
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +2 -0
- package/dist/components/ApplicationScope.js +3 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/Scope.d.ts +8 -0
- package/dist/components/Scope.js +11 -0
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/anthropic/index.d.ts +4 -1
- package/dist/components/anthropic/index.js +4 -2
- package/dist/components/anthropic/index.js.map +1 -1
- package/dist/components/bedrock/index.d.ts +4 -1
- package/dist/components/bedrock/index.js +22 -2
- package/dist/components/bedrock/index.js.map +1 -1
- package/dist/components/componentLoader.d.ts +2 -0
- package/dist/components/componentLoader.js +52 -10
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/componentSecrets.d.ts +63 -0
- package/dist/components/componentSecrets.js +405 -0
- package/dist/components/componentSecrets.js.map +1 -0
- package/dist/components/deploymentRecorder.d.ts +3 -0
- package/dist/components/deploymentRecorder.js +4 -3
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/mcp/adapters/fastify.d.ts +2 -0
- package/dist/components/mcp/adapters/fastify.js +1 -0
- package/dist/components/mcp/adapters/fastify.js.map +1 -1
- package/dist/components/mcp/adapters/harperHttp.d.ts +2 -0
- package/dist/components/mcp/adapters/harperHttp.js +1 -0
- package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
- package/dist/components/mcp/audit.d.ts +2 -2
- package/dist/components/mcp/audit.js +20 -5
- package/dist/components/mcp/audit.js.map +1 -1
- package/dist/components/mcp/customResourceRegistry.d.ts +5 -12
- package/dist/components/mcp/customResourceRegistry.js +15 -30
- package/dist/components/mcp/customResourceRegistry.js.map +1 -1
- package/dist/components/mcp/listChanged.d.ts +7 -2
- package/dist/components/mcp/listChanged.js +28 -0
- package/dist/components/mcp/listChanged.js.map +1 -1
- package/dist/components/mcp/quota.d.ts +31 -0
- package/dist/components/mcp/quota.js +155 -0
- package/dist/components/mcp/quota.js.map +1 -0
- package/dist/components/mcp/rateLimit.d.ts +19 -3
- package/dist/components/mcp/rateLimit.js +129 -13
- package/dist/components/mcp/rateLimit.js.map +1 -1
- package/dist/components/mcp/resources.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +39 -1
- package/dist/components/mcp/toolRegistry.js +60 -5
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/application.d.ts +8 -1
- package/dist/components/mcp/tools/application.js +65 -5
- package/dist/components/mcp/tools/application.js.map +1 -1
- package/dist/components/mcp/tools/operations.d.ts +15 -3
- package/dist/components/mcp/tools/operations.js +99 -38
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operationDescriptions.js +4 -4
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operations.js +4 -0
- package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
- package/dist/components/mcp/transport.d.ts +6 -0
- package/dist/components/mcp/transport.js +47 -3
- package/dist/components/mcp/transport.js.map +1 -1
- package/dist/components/ollama/index.d.ts +4 -1
- package/dist/components/ollama/index.js +4 -2
- package/dist/components/ollama/index.js.map +1 -1
- package/dist/components/openai/index.d.ts +4 -1
- package/dist/components/openai/index.js +4 -2
- package/dist/components/openai/index.js.map +1 -1
- package/dist/components/operations.d.ts +39 -0
- package/dist/components/operations.js +194 -2
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.d.ts +38 -0
- package/dist/components/operationsValidation.js +155 -0
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/secretOperations.d.ts +113 -0
- package/dist/components/secretOperations.js +475 -0
- package/dist/components/secretOperations.js.map +1 -0
- package/dist/config/configUtils.d.ts +4 -1
- package/dist/config/configUtils.js +18 -1
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -3
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
- package/dist/dataLayer/readAuditLog.js +11 -0
- package/dist/dataLayer/readAuditLog.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +2 -0
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/globals.d.ts +1 -1
- package/dist/globals.js +1 -0
- package/dist/globals.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +38 -0
- package/dist/resources/DatabaseTransaction.d.ts +31 -3
- package/dist/resources/DatabaseTransaction.js +190 -29
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +1 -1
- package/dist/resources/LMDBTransaction.js +43 -5
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.d.ts +51 -0
- package/dist/resources/PrimaryRocksDatabase.js +194 -0
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -0
- package/dist/resources/RecordEncoder.d.ts +1 -0
- package/dist/resources/RecordEncoder.js +81 -75
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/RequestTarget.d.ts +19 -0
- package/dist/resources/RequestTarget.js.map +1 -1
- package/dist/resources/Resource.d.ts +0 -1
- package/dist/resources/Resource.js +103 -16
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +6 -2
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/Resources.js.map +1 -1
- package/dist/resources/Table.d.ts +6 -4
- package/dist/resources/Table.js +247 -24
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/read.js +25 -28
- package/dist/resources/analytics/read.js.map +1 -1
- package/dist/resources/blob.js +165 -22
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/crdt.d.ts +2 -0
- package/dist/resources/crdt.js +45 -36
- package/dist/resources/crdt.js.map +1 -1
- package/dist/resources/databases.d.ts +1 -0
- package/dist/resources/databases.js +134 -7
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/graphql.js +8 -0
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +35 -5
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +239 -14
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/jsResource.js +6 -0
- package/dist/resources/jsResource.js.map +1 -1
- package/dist/resources/loadEnv.js +30 -3
- package/dist/resources/loadEnv.js.map +1 -1
- package/dist/resources/login.js +2 -2
- package/dist/resources/login.js.map +1 -1
- package/dist/resources/models/Models.js +14 -3
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/embedHook.js +10 -2
- package/dist/resources/models/embedHook.js.map +1 -1
- package/dist/resources/models/openaiStream.d.ts +56 -0
- package/dist/resources/models/openaiStream.js +94 -0
- package/dist/resources/models/openaiStream.js.map +1 -0
- package/dist/resources/replayLogs.js +4 -3
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/search.d.ts +1 -1
- package/dist/resources/search.js +104 -11
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/secretDecryptor.d.ts +54 -0
- package/dist/resources/secretDecryptor.js +131 -0
- package/dist/resources/secretDecryptor.js.map +1 -0
- package/dist/resources/tracked.js +15 -5
- package/dist/resources/tracked.js.map +1 -1
- package/dist/security/auth.js +71 -8
- package/dist/security/auth.js.map +1 -1
- package/dist/security/jsLoader.js +11 -0
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/tokenAuthentication.d.ts +9 -1
- package/dist/security/tokenAuthentication.js +31 -1
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/server/DurableSubscriptionsSession.d.ts +1 -1
- package/dist/server/DurableSubscriptionsSession.js +8 -4
- package/dist/server/DurableSubscriptionsSession.js.map +1 -1
- package/dist/server/REST.js +28 -9
- package/dist/server/REST.js.map +1 -1
- package/dist/server/fastifyRoutes.js +11 -1
- package/dist/server/fastifyRoutes.js.map +1 -1
- package/dist/server/graphqlQuerying.js +4 -3
- package/dist/server/graphqlQuerying.js.map +1 -1
- package/dist/server/http.d.ts +40 -2
- package/dist/server/http.js +560 -77
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +64 -0
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/liveSubscriptionAuth.d.ts +12 -0
- package/dist/server/liveSubscriptionAuth.js +140 -0
- package/dist/server/liveSubscriptionAuth.js.map +1 -0
- package/dist/server/loadRootComponents.js +2 -1
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/middlewareChain.d.ts +44 -1
- package/dist/server/middlewareChain.js +86 -10
- package/dist/server/middlewareChain.js.map +1 -1
- package/dist/server/mqtt.js +2 -1
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/operationsServer.js +1 -1
- package/dist/server/operationsServer.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +9 -0
- package/dist/server/serverHelpers/Headers.js +22 -0
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/JSONStream.js +3 -3
- package/dist/server/serverHelpers/JSONStream.js.map +1 -1
- package/dist/server/serverHelpers/Request.d.ts +52 -1
- package/dist/server/serverHelpers/Request.js +122 -1
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +3 -8
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/progressEmitter.d.ts +19 -0
- package/dist/server/serverHelpers/progressEmitter.js +52 -4
- package/dist/server/serverHelpers/progressEmitter.js.map +1 -1
- package/dist/server/serverHelpers/registeredOperations.d.ts +40 -0
- package/dist/server/serverHelpers/registeredOperations.js +286 -0
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -0
- package/dist/server/serverHelpers/serverHandlers.js +11 -5
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +1 -0
- package/dist/server/serverHelpers/serverUtilities.js +67 -5
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.d.ts +80 -0
- package/dist/server/serverHelpers/uwsServer.js +433 -0
- package/dist/server/serverHelpers/uwsServer.js.map +1 -0
- package/dist/server/static.d.ts +0 -15
- package/dist/server/static.js +161 -2
- package/dist/server/static.js.map +1 -1
- package/dist/server/status/index.d.ts +4 -1
- package/dist/server/status/index.js +64 -4
- package/dist/server/status/index.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +10 -0
- package/dist/server/threads/manageThreads.js +107 -4
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.js +5 -1
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +92 -14
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/upgrade/directives/5-2-0.d.ts +7 -0
- package/dist/upgrade/directives/5-2-0.js +108 -0
- package/dist/upgrade/directives/5-2-0.js.map +1 -0
- package/dist/upgrade/directives/directivesController.js +2 -1
- package/dist/upgrade/directives/directivesController.js.map +1 -1
- package/dist/utility/common_utils.js +5 -0
- package/dist/utility/common_utils.js.map +1 -1
- package/dist/utility/envFile.d.ts +41 -0
- package/dist/utility/envFile.js +221 -0
- package/dist/utility/envFile.js.map +1 -0
- package/dist/utility/globalSchema.d.ts +9 -0
- package/dist/utility/hdbTerms.d.ts +25 -0
- package/dist/utility/hdbTerms.js +31 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +21 -0
- package/dist/utility/logging/harper_logger.js +160 -8
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logRotator.js +29 -17
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/logging/readLog.d.ts +1 -1
- package/dist/utility/logging/readLog.js +305 -2
- package/dist/utility/logging/readLog.js.map +1 -1
- package/dist/utility/operationPermissions.d.ts +18 -0
- package/dist/utility/operationPermissions.js +35 -1
- package/dist/utility/operationPermissions.js.map +1 -1
- package/dist/utility/operation_authorization.d.ts +17 -0
- package/dist/utility/operation_authorization.js +38 -0
- package/dist/utility/operation_authorization.js.map +1 -1
- package/dist/utility/secretEnvelope.d.ts +30 -0
- package/dist/utility/secretEnvelope.js +94 -0
- package/dist/utility/secretEnvelope.js.map +1 -0
- package/dist/utility/signalling.d.ts +7 -0
- package/dist/utility/signalling.js +16 -0
- package/dist/utility/signalling.js.map +1 -1
- package/dist/validation/configValidator.js +70 -24
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +4 -0
- package/json/systemSchema.json +38 -0
- package/npm-shrinkwrap.json +15066 -9347
- package/package.json +13 -8
- package/resources/DESIGN.md +15 -12
- package/resources/DatabaseTransaction.ts +206 -37
- package/resources/LMDBTransaction.ts +43 -9
- package/resources/PrimaryRocksDatabase.ts +201 -0
- package/resources/RecordEncoder.ts +81 -69
- package/resources/RequestTarget.ts +21 -0
- package/resources/Resource.ts +109 -25
- package/resources/ResourceInterface.ts +6 -2
- package/resources/Resources.ts +1 -3
- package/resources/Table.ts +238 -29
- package/resources/analytics/read.ts +25 -30
- package/resources/blob.ts +159 -23
- package/resources/crdt.ts +37 -31
- package/resources/databases.ts +143 -6
- package/resources/graphql.ts +8 -0
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +251 -17
- package/resources/jsResource.ts +6 -0
- package/resources/loadEnv.ts +34 -3
- package/resources/login.ts +2 -2
- package/resources/models/Models.ts +19 -3
- package/resources/models/embedHook.ts +13 -2
- package/resources/models/openaiStream.ts +133 -0
- package/resources/replayLogs.ts +4 -3
- package/resources/search.ts +113 -11
- package/resources/secretDecryptor.ts +159 -0
- package/resources/tracked.ts +24 -17
- package/security/auth.ts +70 -10
- package/security/jsLoader.ts +12 -0
- package/security/tokenAuthentication.ts +42 -1
- package/server/DESIGN.md +10 -0
- package/server/DurableSubscriptionsSession.ts +8 -4
- package/server/REST.ts +26 -9
- package/server/fastifyRoutes.ts +10 -1
- package/server/graphqlQuerying.ts +4 -3
- package/server/http.ts +563 -77
- package/server/itc/serverHandlers.js +70 -0
- package/server/liveSubscriptionAuth.ts +152 -0
- package/server/loadRootComponents.js +2 -1
- package/server/middlewareChain.ts +107 -17
- package/server/mqtt.ts +2 -1
- package/server/operationsServer.ts +2 -2
- package/server/serverHelpers/Headers.ts +24 -0
- package/server/serverHelpers/JSONStream.ts +3 -3
- package/server/serverHelpers/Request.ts +127 -0
- package/server/serverHelpers/contentTypes.ts +3 -8
- package/server/serverHelpers/progressEmitter.ts +55 -5
- package/server/serverHelpers/registeredOperations.ts +259 -0
- package/server/serverHelpers/serverHandlers.js +11 -5
- package/server/serverHelpers/serverUtilities.ts +88 -5
- package/server/serverHelpers/uwsServer.ts +488 -0
- package/server/static.ts +180 -2
- package/server/status/index.ts +72 -4
- package/server/threads/manageThreads.js +102 -4
- package/server/threads/socketRouter.ts +5 -1
- package/server/threads/threadServer.js +93 -10
- package/studio/web/assets/{Chat-SdD1EZca.js → Chat-Cv_2paZE.js} +2 -2
- package/studio/web/assets/{Chat-SdD1EZca.js.map → Chat-Cv_2paZE.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-GMq6tHK9.js → FloatingChat-CPyPIcVR.js} +4 -4
- package/studio/web/assets/{FloatingChat-GMq6tHK9.js.map → FloatingChat-CPyPIcVR.js.map} +1 -1
- package/studio/web/assets/{applications-B5hhv7uA.js → applications-C0jT2vdZ.js} +2 -2
- package/studio/web/assets/{applications-B5hhv7uA.js.map → applications-C0jT2vdZ.js.map} +1 -1
- package/studio/web/assets/{index-VFcpNWgq.js → index-D_GKOkhn.js} +6 -6
- package/studio/web/assets/index-D_GKOkhn.js.map +1 -0
- package/studio/web/assets/{index.lazy-DgcJojIA.js → index.lazy-CAmCIA65.js} +4 -4
- package/studio/web/assets/{index.lazy-DgcJojIA.js.map → index.lazy-CAmCIA65.js.map} +1 -1
- package/studio/web/assets/{profile-BqN-8vzm.js → profile-CaF-4aZe.js} +2 -2
- package/studio/web/assets/{profile-BqN-8vzm.js.map → profile-CaF-4aZe.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-CHFvFq9u.js → setComponentFile-DLW1DHCt.js} +2 -2
- package/studio/web/assets/{setComponentFile-CHFvFq9u.js.map → setComponentFile-DLW1DHCt.js.map} +1 -1
- package/studio/web/assets/{setup-CQ3vWJSr.js → setup-TGCErIhq.js} +2 -2
- package/studio/web/assets/{setup-CQ3vWJSr.js.map → setup-TGCErIhq.js.map} +1 -1
- package/studio/web/assets/{status-BsW5fjuh.js → status-DG0Maoao.js} +2 -2
- package/studio/web/assets/{status-BsW5fjuh.js.map → status-DG0Maoao.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-DITdeB9-.js → swagger-ui-react-8T4SgQ1m.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-DITdeB9-.js.map → swagger-ui-react-8T4SgQ1m.js.map} +1 -1
- package/studio/web/assets/{tsMode-Dyph-OUs.js → tsMode-DJ6Sl24Q.js} +2 -2
- package/studio/web/assets/{tsMode-Dyph-OUs.js.map → tsMode-DJ6Sl24Q.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-D0QTUqex.js → useEntityRestURL-JO2mfWTQ.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-D0QTUqex.js.map → useEntityRestURL-JO2mfWTQ.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-2-0.ts +82 -0
- package/upgrade/directives/directivesController.ts +2 -1
- package/utility/common_utils.ts +5 -0
- package/utility/envFile.ts +218 -0
- package/utility/hdbTerms.ts +31 -0
- package/utility/logging/harper_logger.ts +156 -8
- package/utility/logging/logRotator.ts +24 -14
- package/utility/logging/readLog.ts +323 -2
- package/utility/operationPermissions.ts +35 -1
- package/utility/operation_authorization.ts +74 -1
- package/utility/secretEnvelope.ts +113 -0
- package/utility/signalling.ts +15 -0
- package/validation/configValidator.ts +78 -25
- package/studio/web/assets/index-VFcpNWgq.js.map +0 -1
package/server/http.ts
CHANGED
|
@@ -13,10 +13,11 @@ import * as terms from '../utility/hdbTerms.ts';
|
|
|
13
13
|
import { getConfigPath } from '../config/configUtils.ts';
|
|
14
14
|
import { getTicketKeys, getWorkerIndex } from './threads/manageThreads.js';
|
|
15
15
|
import { createTLSSelector } from '../security/keys.ts';
|
|
16
|
-
import { createSecureServer } from 'node:http2';
|
|
16
|
+
import { createSecureServer, createServer as createH2CServer } from 'node:http2';
|
|
17
17
|
import { createServer as createSecureServerHttp1 } from 'node:https';
|
|
18
18
|
import { createServer, IncomingMessage } from 'node:http';
|
|
19
|
-
import {
|
|
19
|
+
import { createServer as createNetServer } from 'node:net';
|
|
20
|
+
import { Request, BunRequest, UwsRequest, isBun } from './serverHelpers/Request.ts';
|
|
20
21
|
import { appendHeader, Headers, toWriteHeadHeaders } from './serverHelpers/Headers.ts';
|
|
21
22
|
import { Blob } from '../resources/blob.ts';
|
|
22
23
|
import { recordAction, recordActionBinary } from '../resources/analytics/write.ts';
|
|
@@ -27,10 +28,10 @@ import { server, type ServerOptions, type HttpOptions, type UpgradeOptions, Upgr
|
|
|
27
28
|
import { setPortServerMap, SERVERS } from './serverRegistry.ts';
|
|
28
29
|
import { getComponentName } from '../components/componentLoader.ts';
|
|
29
30
|
import { throttle } from './throttle.ts';
|
|
30
|
-
import { makeCallbackChain as buildCallbackChain } from './middlewareChain.ts';
|
|
31
|
+
import { makeCallbackChain as buildCallbackChain, describeChains } from './middlewareChain.ts';
|
|
31
32
|
import { WebSocketServer } from 'ws';
|
|
32
33
|
|
|
33
|
-
const { errorToString } = harperLogger;
|
|
34
|
+
const { errorToString, errorForLog } = harperLogger;
|
|
34
35
|
server.http = httpServer;
|
|
35
36
|
server.request = onRequest;
|
|
36
37
|
server.ws = onWebSocket;
|
|
@@ -51,8 +52,15 @@ let httpOptions: HttpOptions = {};
|
|
|
51
52
|
export const universalHeaders: [string, string][] = [];
|
|
52
53
|
// Bun-specific: stores fetch handler configs per port, used by threadServer.js to call Bun.serve()
|
|
53
54
|
export const bunServeConfigs: Record<string | number, any> = {};
|
|
54
|
-
//
|
|
55
|
-
|
|
55
|
+
// uWS backend (#914): stores { socketPath, secure, handler } configs keyed by UDS path. When
|
|
56
|
+
// HARPER_UWS_UDS is set, the per-worker UDS mirror is served by uWebSockets.js instead of a Node
|
|
57
|
+
// http server; threadServer.js consumes these and calls createUwsServer(). Symphony must forward
|
|
58
|
+
// client identity via X-Forwarded-For (not the PROXY protocol) on these sockets, as the Bun path does.
|
|
59
|
+
export const uwsServeConfigs: Record<string, any> = {};
|
|
60
|
+
// Stores non-function listeners (e.g. Fastify servers) per port for fallback delegation on the
|
|
61
|
+
// backends that don't register their own Node http server (Bun, and the uWS HTTP path). Keeping
|
|
62
|
+
// them out of SERVERS is what prevents a competing Node server from binding the same port.
|
|
63
|
+
const fallbackServers: Record<string | number, any> = {};
|
|
56
64
|
const udsCleanupPaths: { socketPath: string; yamlPath: string }[] = [];
|
|
57
65
|
|
|
58
66
|
export function registerUdsCleanupPaths(socketPath: string, yamlPath: string) {
|
|
@@ -71,9 +79,12 @@ export function cleanupUdsFiles() {
|
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
/** Write YAML metadata for a UDS mirror socket, describing the TLS certs from the corresponding secure server. */
|
|
74
|
-
export function writeUdsMetadata(yamlPath: string, port: number | string, secureServer: any) {
|
|
82
|
+
export function writeUdsMetadata(yamlPath: string, port: number | string, secureServer: any, protocol?: string) {
|
|
75
83
|
const contexts = secureServer.secureContexts;
|
|
76
84
|
let yaml = `pid: ${process.pid}\ntid: ${currentThreadId()}\nport: ${port}\n`;
|
|
85
|
+
// Which application protocol this socket speaks (absent = http/1.1, the historical
|
|
86
|
+
// default) — lets a fronting proxy route by negotiated ALPN.
|
|
87
|
+
if (protocol) yaml += `protocol: ${protocol}\n`;
|
|
77
88
|
yaml += `certificates:\n`;
|
|
78
89
|
if (contexts?.size > 0) {
|
|
79
90
|
const seen = new Set();
|
|
@@ -300,7 +311,16 @@ export function httpServer(listener, options) {
|
|
|
300
311
|
httpResponders[options?.runFirst ? 'unshift' : 'push'](entry);
|
|
301
312
|
} else if (isBun) {
|
|
302
313
|
// On Bun, store non-function listeners (e.g. Fastify's http.Server) for fallback delegation
|
|
303
|
-
|
|
314
|
+
fallbackServers[port] = listener;
|
|
315
|
+
} else if ((httpServers[port] as any)?.uws) {
|
|
316
|
+
// uWS HTTP path (#914, HARPER_UWS_HTTP): the port is backed by uWebSockets.js, not a Node
|
|
317
|
+
// http server, so a raw non-function listener (e.g. Fastify's http.Server via
|
|
318
|
+
// server.http(fastify.server)) must NOT go through registerServer() — that would put it in
|
|
319
|
+
// SERVERS and threadServer would bind a Node http server competing with uWS on the same TCP
|
|
320
|
+
// port. Divert it to the fallback map like the Bun path; makeUwsHandler delegates unhandled
|
|
321
|
+
// requests to it via inject(). The { uws: true } marker is guaranteed present here: the
|
|
322
|
+
// getServer(port) call above (same loop iteration) sets it before this branch runs.
|
|
323
|
+
fallbackServers[port] = listener;
|
|
304
324
|
} else {
|
|
305
325
|
listener.isSecure = secure;
|
|
306
326
|
registerServer(listener, port, false);
|
|
@@ -317,6 +337,31 @@ function getHTTPServer(port: number, secure: boolean, options: ServerOptions) {
|
|
|
317
337
|
if (!httpServers[port]) {
|
|
318
338
|
// TODO: These should all come from httpOptions or operationsApiOptions
|
|
319
339
|
const serverPrefix = isOperationsServer ? 'operationsApi_network' : (usageType ?? 'http');
|
|
340
|
+
// uWS plaintext-HTTP path (#914, HARPER_UWS_HTTP): back a non-secure TCP HTTP port with
|
|
341
|
+
// uWebSockets.js directly instead of a Node http server. This is the flag used to run the
|
|
342
|
+
// integration suite through uWS (no symphony/UDS needed). WebSocket upgrades are wired on this
|
|
343
|
+
// path via onWebSocket's uWS branch; it's opt-in and separate from HARPER_UWS_UDS.
|
|
344
|
+
const lastColon = String(port).lastIndexOf(':');
|
|
345
|
+
const uwsPort = lastColon > 0 ? +String(port).slice(lastColon + 1) : +port;
|
|
346
|
+
if (
|
|
347
|
+
process.env.HARPER_UWS_HTTP &&
|
|
348
|
+
!secure &&
|
|
349
|
+
!isOperationsServer &&
|
|
350
|
+
!String(port).includes('/') &&
|
|
351
|
+
!Number.isNaN(uwsPort)
|
|
352
|
+
) {
|
|
353
|
+
uwsServeConfigs[port] = {
|
|
354
|
+
port: uwsPort,
|
|
355
|
+
host: lastColon > 0 ? String(port).slice(0, lastColon).replace(/[[\]]/g, '') : undefined,
|
|
356
|
+
secure: false,
|
|
357
|
+
handler: makeUwsHandler(port, isOperationsServer, env.get(serverPrefix + '_requestQueueLimit')),
|
|
358
|
+
};
|
|
359
|
+
// Marker so the httpServers guard is satisfied and the caller has a truthy handle; the
|
|
360
|
+
// actual listen happens in threadServer.js from uwsServeConfigs. onWebSocket() detects
|
|
361
|
+
// this marker (server.uws) and wires native uWS WebSocket handling into the same config.
|
|
362
|
+
httpServers[port] = { uws: true, port } as any;
|
|
363
|
+
return httpServers[port];
|
|
364
|
+
}
|
|
320
365
|
const keepAliveTimeout = env.get(serverPrefix + '_keepAliveTimeout');
|
|
321
366
|
const requestTimeout = env.get(serverPrefix + '_timeout');
|
|
322
367
|
const headersTimeout = env.get(serverPrefix + '_headersTimeout');
|
|
@@ -500,17 +545,19 @@ function getHTTPServer(port: number, secure: boolean, options: ServerOptions) {
|
|
|
500
545
|
}
|
|
501
546
|
function onError(error) {
|
|
502
547
|
const headers = error.headers;
|
|
503
|
-
|
|
548
|
+
// the HTTP status may be carried as `statusCode` (our error classes) or `status` (e.g. a thrown plain object)
|
|
549
|
+
const statusCode = error.statusCode ?? error.status;
|
|
550
|
+
const status = statusCode || 500;
|
|
504
551
|
try {
|
|
505
552
|
nodeResponse.writeHead(status, toWriteHeadHeaders(headers));
|
|
506
553
|
} catch {} // silently ignore errors writing headers, because they may have been set already
|
|
507
554
|
nodeResponse.end(errorToString(error));
|
|
508
555
|
logRequest(nodeRequest, status, requestId, performance.now() - startTime);
|
|
509
556
|
// a status code is interpreted as an expected error, so just info or warn, otherwise log as error
|
|
510
|
-
if (
|
|
511
|
-
if (
|
|
512
|
-
else harperLogger.info(error);
|
|
513
|
-
} else harperLogger.error(error);
|
|
557
|
+
if (statusCode) {
|
|
558
|
+
if (statusCode === 500) harperLogger.warn(errorForLog(error));
|
|
559
|
+
else harperLogger.info(errorForLog(error));
|
|
560
|
+
} else harperLogger.error(errorForLog(error));
|
|
514
561
|
}
|
|
515
562
|
};
|
|
516
563
|
// create a throttled version of the request handler, so we can throttle POST requests
|
|
@@ -571,38 +618,281 @@ function getHTTPServer(port: number, secure: boolean, options: ServerOptions) {
|
|
|
571
618
|
const udsPath = join(socketsDir, `${socketName}.sock`);
|
|
572
619
|
const yamlPath = join(socketsDir, `${socketName}.yaml`);
|
|
573
620
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
621
|
+
if (process.env.HARPER_UWS_UDS) {
|
|
622
|
+
// uWS backend (#914): serve the UDS mirror with uWebSockets.js instead of a Node http
|
|
623
|
+
// server. threadServer.js consumes uwsServeConfigs and calls createUwsServer(). uWS does
|
|
624
|
+
// not parse the PROXY protocol, so symphony must use sourceAddressHeader: 'xForwardedFor'
|
|
625
|
+
// for this socket (the same mode it uses for the Bun path).
|
|
626
|
+
uwsServeConfigs[udsPath] = {
|
|
627
|
+
socketPath: udsPath,
|
|
628
|
+
secure: true,
|
|
629
|
+
handler: makeUwsHandler(port, isOperationsServer, env.get(serverPrefix + '_requestQueueLimit')),
|
|
630
|
+
};
|
|
631
|
+
} else {
|
|
632
|
+
// Create a plain HTTP server (no TLS) with the same request handler
|
|
633
|
+
const udsServer = createServer(
|
|
634
|
+
{
|
|
635
|
+
keepAliveTimeout,
|
|
636
|
+
headersTimeout,
|
|
637
|
+
requestTimeout,
|
|
638
|
+
highWaterMark: 128 * 1024,
|
|
639
|
+
noDelay: true,
|
|
640
|
+
keepAlive: true,
|
|
641
|
+
keepAliveInitialDelay: 600,
|
|
642
|
+
maxHeaderSize: env.get(terms.CONFIG_PARAMS.HTTP_MAXHEADERSIZE),
|
|
643
|
+
},
|
|
644
|
+
(nodeRequest: IncomingMessage, nodeResponse: any) => {
|
|
645
|
+
const method = nodeRequest.method;
|
|
646
|
+
if (method === 'GET' || method === 'OPTIONS' || method === 'HEAD')
|
|
647
|
+
requestHandler(nodeRequest, nodeResponse);
|
|
648
|
+
else throttledRequestHandler(nodeRequest, nodeResponse);
|
|
649
|
+
}
|
|
650
|
+
);
|
|
592
651
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
652
|
+
udsServer.isPerThreadSocket = true;
|
|
653
|
+
enableProxyProtocol(udsServer);
|
|
654
|
+
SERVERS[udsPath] = udsServer;
|
|
655
|
+
}
|
|
596
656
|
registerUdsCleanupPaths(udsPath, yamlPath);
|
|
597
657
|
|
|
598
658
|
const writeMetadata = () => writeUdsMetadata(yamlPath, port, server);
|
|
599
659
|
options.SNICallback.ready.then(writeMetadata);
|
|
600
660
|
server.secureContextsListeners.push(writeMetadata);
|
|
661
|
+
|
|
662
|
+
// Optional cleartext HTTP/2 mirror (spike: HARPER_H2C_UDS=1). A separate socket
|
|
663
|
+
// (`<worker>-<port>-h2.sock`) so a fronting proxy can route by negotiated ALPN:
|
|
664
|
+
// h2 connections here, http/1.1 to the plain mirror above. The metadata yaml
|
|
665
|
+
// carries `protocol: h2` so the proxy can discover which socket speaks what.
|
|
666
|
+
if (process.env.HARPER_H2C_UDS) {
|
|
667
|
+
const udsPathH2 = join(socketsDir, `${socketName}-h2.sock`);
|
|
668
|
+
const yamlPathH2 = join(socketsDir, `${socketName}-h2.yaml`);
|
|
669
|
+
const h2Server = createH2CServer({}, (nodeRequest: any, nodeResponse: any) => {
|
|
670
|
+
const method = nodeRequest.method;
|
|
671
|
+
if (method === 'GET' || method === 'OPTIONS' || method === 'HEAD') requestHandler(nodeRequest, nodeResponse);
|
|
672
|
+
else throttledRequestHandler(nodeRequest, nodeResponse);
|
|
673
|
+
});
|
|
674
|
+
// A stray non-h2 client (or a truncated preface) fails the session, not the worker.
|
|
675
|
+
h2Server.on('sessionError', (error: Error) => {
|
|
676
|
+
harperLogger.debug('h2c UDS session error:', error);
|
|
677
|
+
});
|
|
678
|
+
const h2Front = createH2CProxyFront(h2Server) as any;
|
|
679
|
+
h2Front.isPerThreadSocket = true;
|
|
680
|
+
SERVERS[udsPathH2] = h2Front;
|
|
681
|
+
registerUdsCleanupPaths(udsPathH2, yamlPathH2);
|
|
682
|
+
|
|
683
|
+
const writeMetadataH2 = () => writeUdsMetadata(yamlPathH2, port, server, 'h2');
|
|
684
|
+
options.SNICallback.ready.then(writeMetadataH2);
|
|
685
|
+
server.secureContextsListeners.push(writeMetadataH2);
|
|
686
|
+
}
|
|
601
687
|
}
|
|
602
688
|
}
|
|
603
689
|
return httpServers[port];
|
|
604
690
|
}
|
|
605
691
|
|
|
692
|
+
/**
|
|
693
|
+
* uWS backend (#914): builds the per-request handler for a uWS UDS server. Mirrors the Bun
|
|
694
|
+
* fetchHandler's post-processing (httpChain, unhandled, universalHeaders, Server-Timing,
|
|
695
|
+
* analytics, logging) but returns a plain Harper response descriptor for createUwsServer to
|
|
696
|
+
* serialize onto the uWS HttpResponse. When the chain doesn't handle the request (status === -1)
|
|
697
|
+
* and a Fastify fallback is registered for the port, it delegates via inject() (see injectToFastify),
|
|
698
|
+
* mirroring the Bun path — so legacy Fastify routes work behind uWS too.
|
|
699
|
+
*/
|
|
700
|
+
function makeUwsHandler(port: number | string, isOperationsServer: boolean, requestQueueLimit?: number) {
|
|
701
|
+
// Build a fresh response descriptor rather than mutating what the chain returned: a handler may
|
|
702
|
+
// return a WHATWG `Response` (read-only `status`/`body` accessors), which the Bun path also never
|
|
703
|
+
// mutates. `headers` is normalized in place the same way the Bun path does.
|
|
704
|
+
const handle = async (request: any) => {
|
|
705
|
+
const startTime = performance.now();
|
|
706
|
+
let requestId = 0;
|
|
707
|
+
if (isOperationsServer) request.isOperationsServer = true;
|
|
708
|
+
if (httpOptions.logging?.id) request.requestId = requestId = getRequestId();
|
|
709
|
+
let response = await httpChain[port](request);
|
|
710
|
+
if (!response) response = unhandled(request);
|
|
711
|
+
let headers = response.headers;
|
|
712
|
+
if (!headers?.set) headers = new Headers(headers);
|
|
713
|
+
for (const [key, value] of universalHeaders) headers.set(key, value);
|
|
714
|
+
if (response.status === -1) {
|
|
715
|
+
// The chain didn't handle it. If a Fastify fallback is registered for this port (legacy
|
|
716
|
+
// custom-function routes via server.http(fastify.server)), delegate to it via inject(),
|
|
717
|
+
// mirroring the Bun path; otherwise it's a genuine 404.
|
|
718
|
+
const fastify = fastifyInstances[port];
|
|
719
|
+
if (fastify) {
|
|
720
|
+
const injectResult = await injectToFastify(fastify, {
|
|
721
|
+
method: request.method,
|
|
722
|
+
url: request.url,
|
|
723
|
+
headers: request.headers.asObject,
|
|
724
|
+
body: request.body, // stream; inject() consumes it as the payload
|
|
725
|
+
user: request.user,
|
|
726
|
+
});
|
|
727
|
+
const respHeaders = new Headers();
|
|
728
|
+
for (const [k, v] of Object.entries(injectResult.headers)) {
|
|
729
|
+
if (v == null) continue;
|
|
730
|
+
// Keep Set-Cookie multi-valued (Harper Headers + writeHeaders emit each separately);
|
|
731
|
+
// only comma-join other repeated headers.
|
|
732
|
+
if (Array.isArray(v)) respHeaders.set(k, k.toLowerCase() === 'set-cookie' ? v : v.join(', '));
|
|
733
|
+
else respHeaders.set(k, String(v));
|
|
734
|
+
}
|
|
735
|
+
logHttpRequest(request, injectResult.statusCode, requestId, performance.now() - startTime);
|
|
736
|
+
const responseStream = injectResult.stream();
|
|
737
|
+
// Event-stream (SSE) responses must reach the client incrementally — stream the body and,
|
|
738
|
+
// on client disconnect, destroy the inject response so the Fastify reply's teardown runs
|
|
739
|
+
// (matches the Bun path). Finite responses buffer so Content-Length stays set.
|
|
740
|
+
if (String(injectResult.headers['content-type'] ?? '').includes('text/event-stream')) {
|
|
741
|
+
const injectResponse = injectResult.raw?.res;
|
|
742
|
+
if (injectResponse && typeof injectResponse.destroy === 'function') {
|
|
743
|
+
responseStream.once('close', () => injectResponse.destroy());
|
|
744
|
+
}
|
|
745
|
+
return { status: injectResult.statusCode, headers: respHeaders, body: responseStream };
|
|
746
|
+
}
|
|
747
|
+
const chunks: Buffer[] = [];
|
|
748
|
+
for await (const chunk of responseStream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
749
|
+
return {
|
|
750
|
+
status: injectResult.statusCode,
|
|
751
|
+
headers: respHeaders,
|
|
752
|
+
body: chunks.length > 0 ? Buffer.concat(chunks) : null,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
logHttpRequest(request, 404, requestId, performance.now() - startTime);
|
|
756
|
+
return { status: 404, headers: new Headers({ 'content-type': 'text/plain' }), body: 'Not found\n' };
|
|
757
|
+
}
|
|
758
|
+
const status = response.status || 200;
|
|
759
|
+
const executionTime = performance.now() - startTime;
|
|
760
|
+
if (!response.handlesHeaders) {
|
|
761
|
+
let serverTiming = `hdb;dur=${executionTime.toFixed(2)}`;
|
|
762
|
+
if (response.wasCacheMiss) serverTiming += ', miss';
|
|
763
|
+
appendHeader(headers, 'Server-Timing', serverTiming, true);
|
|
764
|
+
}
|
|
765
|
+
recordAction(
|
|
766
|
+
executionTime,
|
|
767
|
+
'duration',
|
|
768
|
+
request.handlerPath,
|
|
769
|
+
request.method,
|
|
770
|
+
response.wasCacheMiss == undefined ? undefined : response.wasCacheMiss ? 'cache-miss' : 'cache-hit'
|
|
771
|
+
);
|
|
772
|
+
recordActionBinary(status < 400, 'success', request.handlerPath, request.method);
|
|
773
|
+
recordActionBinary(1, 'response_' + status, request.handlerPath, request.method);
|
|
774
|
+
logHttpRequest(request, status, requestId, executionTime);
|
|
775
|
+
// Static handlers (the only handlesHeaders producers) return a `send` SendStream that writes
|
|
776
|
+
// its own headers/body to a Node ServerResponse via .pipe(). uWS has no such object, and a
|
|
777
|
+
// SendStream doesn't start until piped, so streaming it directly hangs (headers never flush).
|
|
778
|
+
// Pipe it into a Writable shim that captures the headers and buffers the file, mirroring the
|
|
779
|
+
// Bun path. Non-handlesHeaders bodies keep streaming through normalizeUwsBody.
|
|
780
|
+
if (response.handlesHeaders && response.body && typeof response.body.pipe === 'function') {
|
|
781
|
+
// send() may return 304 (conditional GET) or 206/416 (Range) — honor the status it set.
|
|
782
|
+
const sent = await bufferSendStream(response.body, headers, status, request.signal);
|
|
783
|
+
return { status: sent.status, headers, handlesHeaders: true, body: sent.body };
|
|
784
|
+
}
|
|
785
|
+
const body = await normalizeUwsBody(response.body, request.signal);
|
|
786
|
+
return { status, headers, handlesHeaders: response.handlesHeaders, body };
|
|
787
|
+
};
|
|
788
|
+
// Shed data-modifying requests when the event queue is backed up (503), mirroring the Node UDS
|
|
789
|
+
// path — GET/OPTIONS/HEAD are cheap and always run, everything else goes through the throttle.
|
|
790
|
+
const throttledHandle = throttle(
|
|
791
|
+
handle,
|
|
792
|
+
(_request: any) => {
|
|
793
|
+
recordAction(true, 'service-unavailable', port);
|
|
794
|
+
return {
|
|
795
|
+
status: 503,
|
|
796
|
+
headers: new Headers({ 'content-type': 'text/plain' }),
|
|
797
|
+
body: 'Service unavailable, exceeded request queue limit',
|
|
798
|
+
};
|
|
799
|
+
},
|
|
800
|
+
requestQueueLimit
|
|
801
|
+
);
|
|
802
|
+
return (request: any) => {
|
|
803
|
+
const method = request.method;
|
|
804
|
+
if (method === 'GET' || method === 'OPTIONS' || method === 'HEAD') return handle(request);
|
|
805
|
+
return throttledHandle(request);
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* uWS: normalize a Harper response body into what the adapter can serialize. Finite bodies collapse
|
|
811
|
+
* to a string/Buffer; a Node stream or async-iterable is returned as a Readable so writeResponse can
|
|
812
|
+
* stream it incrementally (buffering an SSE/event-stream body here would never return). `signal`
|
|
813
|
+
* aborts the collapse of a sync iterable if the client disconnects mid-response.
|
|
814
|
+
*/
|
|
815
|
+
/**
|
|
816
|
+
* Drive a `send` SendStream to completion against a Writable shim, capturing the headers it writes
|
|
817
|
+
* (setHeader/writeHead) onto `headers` and the status it sets (statusCode/writeHead) and buffering
|
|
818
|
+
* the file body. `send` targets an http.ServerResponse (setHeader/writeHead/statusCode/finished);
|
|
819
|
+
* uWS has none, so we adapt — mirrors the Bun fetchHandler's SendStream path. The captured status
|
|
820
|
+
* carries send's conditional-GET (304) and Range (206/416) results. Buffering is fine for static
|
|
821
|
+
* assets and keeps Content-Length set. `defaultStatus` is used when send sets none.
|
|
822
|
+
*/
|
|
823
|
+
function bufferSendStream(
|
|
824
|
+
body: any,
|
|
825
|
+
headers: Headers,
|
|
826
|
+
defaultStatus: number,
|
|
827
|
+
signal?: AbortSignal
|
|
828
|
+
): Promise<{ body: Buffer; status: number }> {
|
|
829
|
+
return new Promise((resolve, reject) => {
|
|
830
|
+
const chunks: Buffer[] = [];
|
|
831
|
+
const dest: any = new Writable({
|
|
832
|
+
write(chunk, _encoding, callback) {
|
|
833
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
834
|
+
callback();
|
|
835
|
+
},
|
|
836
|
+
final(callback) {
|
|
837
|
+
callback();
|
|
838
|
+
resolve({ body: Buffer.concat(chunks), status: dest.statusCode || defaultStatus });
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
Object.assign(dest, {
|
|
842
|
+
setHeader: (n: string, v: any) => headers.set(n, v),
|
|
843
|
+
getHeader: (n: string) => headers.get(n),
|
|
844
|
+
removeHeader: (n: string) => (headers as any).delete(n.toLowerCase()),
|
|
845
|
+
// send conveys 304/206/416 via statusCode and/or writeHead's status arg — capture both so
|
|
846
|
+
// conditional-GET and Range responses aren't flattened to the default 200.
|
|
847
|
+
writeHead: (s: number, hdrs?: any) => {
|
|
848
|
+
if (s) dest.statusCode = s;
|
|
849
|
+
if (hdrs) for (const k in hdrs) headers.set(k, hdrs[k]);
|
|
850
|
+
},
|
|
851
|
+
statusCode: defaultStatus,
|
|
852
|
+
headersSent: false,
|
|
853
|
+
// 'on-finished' (used by 'send') treats a non-false `finished` as already-done and destroys
|
|
854
|
+
// the read stream before data flows; keep it false so it waits for the 'finish' event.
|
|
855
|
+
finished: false,
|
|
856
|
+
});
|
|
857
|
+
const onAbort = () => {
|
|
858
|
+
body.destroy?.();
|
|
859
|
+
dest.destroy?.();
|
|
860
|
+
reject(new Error('client aborted'));
|
|
861
|
+
};
|
|
862
|
+
if (signal) {
|
|
863
|
+
if (signal.aborted) return onAbort();
|
|
864
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
865
|
+
}
|
|
866
|
+
body.on('error', reject);
|
|
867
|
+
dest.on('error', reject);
|
|
868
|
+
body.pipe(dest);
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async function normalizeUwsBody(
|
|
873
|
+
body: any,
|
|
874
|
+
signal?: AbortSignal
|
|
875
|
+
): Promise<string | Buffer | Uint8Array | Readable | null> {
|
|
876
|
+
if (body == null) return null;
|
|
877
|
+
if (typeof body === 'string' || Buffer.isBuffer(body) || body instanceof Uint8Array) return body;
|
|
878
|
+
if (body instanceof Blob) return Buffer.from(await body.arrayBuffer());
|
|
879
|
+
if (typeof body.then === 'function') return normalizeUwsBody(await body, signal);
|
|
880
|
+
// Already a Node stream — stream it as-is (re-wrapping in Readable.from breaks destroy propagation).
|
|
881
|
+
if (typeof body.pipe === 'function') return body;
|
|
882
|
+
// Async-iterable (e.g. an event queue) — adapt to a Readable and stream it.
|
|
883
|
+
if (body[Symbol.asyncIterator]) return Readable.from(body);
|
|
884
|
+
// Sync iterable — small/finite, collapse to a buffer.
|
|
885
|
+
if (body[Symbol.iterator]) {
|
|
886
|
+
const chunks: Buffer[] = [];
|
|
887
|
+
for (const chunk of body) {
|
|
888
|
+
if (signal?.aborted) throw new Error('client aborted');
|
|
889
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
890
|
+
}
|
|
891
|
+
return Buffer.concat(chunks);
|
|
892
|
+
}
|
|
893
|
+
return String(body);
|
|
894
|
+
}
|
|
895
|
+
|
|
606
896
|
/**
|
|
607
897
|
* Bun-specific HTTP server setup. Instead of creating a Node http.Server, we store a fetch handler config
|
|
608
898
|
* that will be passed to Bun.serve() when listenOnPorts() is called in threadServer.js.
|
|
@@ -632,14 +922,14 @@ function getBunHTTPServer(port: number, secure: boolean, options: ServerOptions)
|
|
|
632
922
|
response.headers.set(key, value);
|
|
633
923
|
}
|
|
634
924
|
if (response.status === -1) {
|
|
635
|
-
const fallbackServer =
|
|
925
|
+
const fallbackServer = fallbackServers[port];
|
|
636
926
|
if (fallbackServer) {
|
|
637
927
|
// Delegate to the fallback server (e.g. Fastify) via node:http compatibility.
|
|
638
928
|
// We create a Node-compatible IncomingMessage/ServerResponse and emit 'request'
|
|
639
929
|
// on the fallback server, then capture the response.
|
|
640
930
|
return await bunDelegateToNodeServer(fallbackServer, webRequest, request);
|
|
641
931
|
}
|
|
642
|
-
|
|
932
|
+
logHttpRequest(request, 404, requestId, performance.now() - startTime);
|
|
643
933
|
return new Response('Not found\n', { status: 404 });
|
|
644
934
|
}
|
|
645
935
|
const status = response.status || 200;
|
|
@@ -693,7 +983,7 @@ function getBunHTTPServer(port: number, secure: boolean, options: ServerOptions)
|
|
|
693
983
|
);
|
|
694
984
|
recordActionBinary(status < 400, 'success', handlerPath, method);
|
|
695
985
|
recordActionBinary(1, 'response_' + status, handlerPath, method);
|
|
696
|
-
|
|
986
|
+
logHttpRequest(request, status, requestId, executionTime);
|
|
697
987
|
// Convert body to something Bun's Response can accept
|
|
698
988
|
if (body instanceof ReadableStream) {
|
|
699
989
|
return new Response(body, { status, headers: responseHeaders });
|
|
@@ -745,12 +1035,14 @@ function getBunHTTPServer(port: number, secure: boolean, options: ServerOptions)
|
|
|
745
1035
|
}
|
|
746
1036
|
return new Response(body, { status, headers: responseHeaders });
|
|
747
1037
|
} catch (error) {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1038
|
+
// the HTTP status may be carried as `statusCode` (our error classes) or `status` (e.g. a thrown plain object)
|
|
1039
|
+
const statusCode = error.statusCode ?? error.status;
|
|
1040
|
+
const status = statusCode || 500;
|
|
1041
|
+
logHttpRequest(null, status, requestId, performance.now() - startTime);
|
|
1042
|
+
if (statusCode) {
|
|
1043
|
+
if (statusCode === 500) harperLogger.warn(errorForLog(error));
|
|
1044
|
+
else harperLogger.info(errorForLog(error));
|
|
1045
|
+
} else harperLogger.error(errorForLog(error));
|
|
754
1046
|
return new Response(errorToString(error), { status });
|
|
755
1047
|
}
|
|
756
1048
|
};
|
|
@@ -789,46 +1081,61 @@ function getBunHTTPServer(port: number, secure: boolean, options: ServerOptions)
|
|
|
789
1081
|
* Bridge a Bun fetch request to a Node.js http.Server (e.g. Fastify) by using Fastify's inject()
|
|
790
1082
|
* method to send the request through its internal router without needing a real socket.
|
|
791
1083
|
*/
|
|
792
|
-
let
|
|
793
|
-
export function
|
|
794
|
-
|
|
1084
|
+
let fastifyInstances: Record<string | number, any> = {};
|
|
1085
|
+
export function registerFastifyInstance(port: string | number, instance: any) {
|
|
1086
|
+
fastifyInstances[port] = instance;
|
|
795
1087
|
}
|
|
796
1088
|
const INTERNAL_USER_HEADER = 'x-harper-internal-pre-auth-user';
|
|
797
1089
|
|
|
1090
|
+
/**
|
|
1091
|
+
* Run a request through a Fastify instance via inject() — its internal router, no socket needed.
|
|
1092
|
+
* Shared by the Bun and uWS fallback-delegation paths. Strips any forged pre-auth header from the
|
|
1093
|
+
* client and, when Harper's auth middleware resolved a user without credentials (e.g. AUTHORIZE_LOCAL
|
|
1094
|
+
* for loopback in dev), forwards it so Fastify can skip its own auth — only when no Authorization
|
|
1095
|
+
* header was supplied, otherwise Fastify's Passport validates the credentials normally.
|
|
1096
|
+
* `payloadAsStream` makes inject() resolve as soon as the response headers are written and exposes
|
|
1097
|
+
* the body as a Readable, so a long-lived SSE response (the MCP server-push GET) streams instead of
|
|
1098
|
+
* buffering forever.
|
|
1099
|
+
*/
|
|
1100
|
+
function injectToFastify(
|
|
1101
|
+
fastify: any,
|
|
1102
|
+
req: { method: string; url: string; headers: Record<string, any>; body?: Buffer | Readable; user?: any }
|
|
1103
|
+
) {
|
|
1104
|
+
const headers: Record<string, any> = {};
|
|
1105
|
+
for (const key in req.headers) {
|
|
1106
|
+
if (key.toLowerCase() !== INTERNAL_USER_HEADER) headers[key] = req.headers[key];
|
|
1107
|
+
}
|
|
1108
|
+
// Both callers pass already-lowercased header keys (uWS lowercases at the protocol level →
|
|
1109
|
+
// RequestHeaders.asObject; Bun's webRequest.headers.forEach yields lowercase), so the literal
|
|
1110
|
+
// 'authorization' lookup is reliable — the pre-auth user is only forwarded when the client sent
|
|
1111
|
+
// no credentials of its own.
|
|
1112
|
+
if (req.user && !headers['authorization']) {
|
|
1113
|
+
headers[INTERNAL_USER_HEADER] = JSON.stringify(req.user);
|
|
1114
|
+
}
|
|
1115
|
+
return fastify.inject({ method: req.method, url: req.url, headers, payload: req.body, payloadAsStream: true });
|
|
1116
|
+
}
|
|
1117
|
+
|
|
798
1118
|
async function bunDelegateToNodeServer(
|
|
799
1119
|
nodeServer: any,
|
|
800
1120
|
webRequest: globalThis.Request,
|
|
801
1121
|
bunRequest?: any
|
|
802
1122
|
): Promise<Response> {
|
|
803
1123
|
// Check if there's a Fastify instance registered for this port (preferred path)
|
|
804
|
-
for (const port in
|
|
805
|
-
if (
|
|
806
|
-
const fastify =
|
|
1124
|
+
for (const port in fallbackServers) {
|
|
1125
|
+
if (fallbackServers[port] === nodeServer && fastifyInstances[port]) {
|
|
1126
|
+
const fastify = fastifyInstances[port];
|
|
807
1127
|
const url = new URL(webRequest.url);
|
|
808
1128
|
const body = webRequest.body ? Buffer.from(await webRequest.arrayBuffer()) : undefined;
|
|
809
1129
|
const headers: Record<string, string> = {};
|
|
810
1130
|
webRequest.headers.forEach((value, key) => {
|
|
811
|
-
|
|
812
|
-
if (key.toLowerCase() !== INTERNAL_USER_HEADER) headers[key] = value;
|
|
1131
|
+
headers[key] = value;
|
|
813
1132
|
});
|
|
814
|
-
|
|
815
|
-
// AUTHORIZE_LOCAL for loopback connections in dev mode), pass the user so Fastify can
|
|
816
|
-
// skip its own auth. Only applies when there is no Authorization header — if credentials
|
|
817
|
-
// were provided, let Fastify's Passport validate them normally.
|
|
818
|
-
if (bunRequest?.user && !headers['authorization']) {
|
|
819
|
-
headers[INTERNAL_USER_HEADER] = JSON.stringify(bunRequest.user);
|
|
820
|
-
}
|
|
821
|
-
// `payloadAsStream` makes inject() resolve as soon as the response headers are
|
|
822
|
-
// written and exposes the body as a Readable, instead of buffering the whole
|
|
823
|
-
// payload and resolving only on response end. Without it a long-lived SSE
|
|
824
|
-
// response (the MCP server-push GET) never ends, so `await inject()` would
|
|
825
|
-
// hang forever and the client would never receive headers.
|
|
826
|
-
const injectResult = await fastify.inject({
|
|
1133
|
+
const injectResult = await injectToFastify(fastify, {
|
|
827
1134
|
method: webRequest.method,
|
|
828
1135
|
url: url.pathname + url.search,
|
|
829
1136
|
headers,
|
|
830
|
-
|
|
831
|
-
|
|
1137
|
+
body,
|
|
1138
|
+
user: bunRequest?.user,
|
|
832
1139
|
});
|
|
833
1140
|
const webHeaders = new globalThis.Headers();
|
|
834
1141
|
for (const [k, v] of Object.entries(injectResult.headers)) {
|
|
@@ -874,22 +1181,87 @@ async function bunDelegateToNodeServer(
|
|
|
874
1181
|
return new Response('Not found\n', { status: 404 });
|
|
875
1182
|
}
|
|
876
1183
|
|
|
877
|
-
|
|
1184
|
+
type SerializedRoute = { host?: string; urlPath?: string; order: string[] };
|
|
1185
|
+
// Resolved order captured at chain-build time, keyed identically to httpChain/upgradeChains/
|
|
1186
|
+
// websocketChains (kind → port → routes). Reporting the stored build-time order rather than
|
|
1187
|
+
// recomputing from current responders guarantees get_status matches the callback chain actually
|
|
1188
|
+
// serving that port — including cases where a late `port: 'all'` registration rebuilds only the
|
|
1189
|
+
// 'all' chain and leaves a concrete port's chain (and this description) unchanged (#1573).
|
|
1190
|
+
const resolvedChainDescriptions: Record<string, Record<string, SerializedRoute[]>> = {
|
|
1191
|
+
http: {},
|
|
1192
|
+
upgrade: {},
|
|
1193
|
+
websocket: {},
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
function makeCallbackChain(
|
|
1197
|
+
responders: typeof httpResponders,
|
|
1198
|
+
portNum: number | string,
|
|
1199
|
+
requestArgIndex: number = 0,
|
|
1200
|
+
kind: string = 'http'
|
|
1201
|
+
) {
|
|
1202
|
+
const onCycle = () => {
|
|
1203
|
+
harperLogger.warn(
|
|
1204
|
+
`Cycle detected in ${kind} middleware before/after ordering on port ${portNum}; falling back to registration order.`
|
|
1205
|
+
);
|
|
1206
|
+
};
|
|
1207
|
+
// describeChains reuses the same resolvers as buildCallbackChain, so this is the served order.
|
|
1208
|
+
// onCycle is omitted: the build call owns the single cycle warning, and on a cycle describeChains
|
|
1209
|
+
// falls back to registration order exactly as the built chain does.
|
|
1210
|
+
const routes: SerializedRoute[] = describeChains(responders, portNum).map((route) => ({
|
|
1211
|
+
host: route.host,
|
|
1212
|
+
urlPath: route.urlPath,
|
|
1213
|
+
order: route.order.map((entry) => entry.name ?? '(anonymous)'),
|
|
1214
|
+
}));
|
|
1215
|
+
resolvedChainDescriptions[kind][portNum] = routes;
|
|
1216
|
+
if (harperLogger.debug) {
|
|
1217
|
+
for (const route of routes) {
|
|
1218
|
+
const scope = route.host || route.urlPath ? ` [${route.host ?? '*'}${route.urlPath ?? ''}]` : '';
|
|
1219
|
+
harperLogger.debug(
|
|
1220
|
+
`Resolved ${kind} middleware chain on port ${portNum}${scope}: ${route.order.join(' → ') || '(empty)'}`
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
878
1224
|
return buildCallbackChain(
|
|
879
1225
|
responders,
|
|
880
1226
|
portNum,
|
|
881
1227
|
unhandled,
|
|
882
|
-
|
|
1228
|
+
onCycle,
|
|
1229
|
+
requestArgIndex,
|
|
1230
|
+
({ entryName, kind: refKind, target }) => {
|
|
883
1231
|
harperLogger.warn(
|
|
884
|
-
`
|
|
1232
|
+
`Middleware ordering: ${entryName ? `'${entryName}'` : 'a handler'} requested \`${refKind}: '${target}'\` but no handler named '${target}' is registered on port ${portNum}, so the constraint is ignored. Handler names are the config keys as registered (e.g. 'rest').`
|
|
885
1233
|
);
|
|
886
|
-
}
|
|
887
|
-
requestArgIndex
|
|
1234
|
+
}
|
|
888
1235
|
);
|
|
889
1236
|
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Returns the resolved middleware order for every built HTTP, upgrade, and WebSocket chain on the
|
|
1240
|
+
* current thread, as plain serializable data (listeners omitted). Surfaced via the `get_status`
|
|
1241
|
+
* operation so chain placement can be verified on a running instance (#1573). The 'all' pseudo-port
|
|
1242
|
+
* is excluded: makeCallbackChain builds a chain for it, but it is not a bound listener (its
|
|
1243
|
+
* responders already fold into every concrete port's chain).
|
|
1244
|
+
*
|
|
1245
|
+
* Note: this reflects the calling thread's built chains. All HTTP worker threads register
|
|
1246
|
+
* identically, so any worker's view is representative.
|
|
1247
|
+
*/
|
|
1248
|
+
export function describeMiddlewareChains() {
|
|
1249
|
+
const concretePorts = (byPort: Record<string, SerializedRoute[]>) => {
|
|
1250
|
+
const out: Record<string, SerializedRoute[]> = {};
|
|
1251
|
+
for (const port of Object.keys(byPort)) if (port !== 'all') out[port] = byPort[port];
|
|
1252
|
+
return out;
|
|
1253
|
+
};
|
|
1254
|
+
return {
|
|
1255
|
+
http: concretePorts(resolvedChainDescriptions.http),
|
|
1256
|
+
upgrade: concretePorts(resolvedChainDescriptions.upgrade),
|
|
1257
|
+
websocket: concretePorts(resolvedChainDescriptions.websocket),
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
890
1260
|
function unhandled(request) {
|
|
891
|
-
if (request.user) {
|
|
892
|
-
// pass on authentication information to the next server
|
|
1261
|
+
if (request.user && request._nodeRequest) {
|
|
1262
|
+
// pass on authentication information to the next server (Node fallback delegation via the
|
|
1263
|
+
// 'unhandled' event chain). The Bun/uWS adapters have no _nodeRequest; they forward the
|
|
1264
|
+
// resolved user to the Fastify fallback via injectToFastify's INTERNAL_USER_HEADER instead.
|
|
893
1265
|
request._nodeRequest.user = request.user;
|
|
894
1266
|
}
|
|
895
1267
|
return {
|
|
@@ -929,7 +1301,7 @@ function onUpgrade(listener: UpgradeListener, options: UpgradeOptions) {
|
|
|
929
1301
|
host: options?.host || undefined,
|
|
930
1302
|
};
|
|
931
1303
|
upgradeListeners[options?.runFirst ? 'unshift' : 'push'](entry);
|
|
932
|
-
upgradeChains[port] = makeCallbackChain(upgradeListeners, port);
|
|
1304
|
+
upgradeChains[port] = makeCallbackChain(upgradeListeners, port, 0, 'upgrade');
|
|
933
1305
|
}
|
|
934
1306
|
}
|
|
935
1307
|
|
|
@@ -965,7 +1337,35 @@ function onWebSocket(listener: (ws: WebSocket) => void, options: OnWebSocketOpti
|
|
|
965
1337
|
|
|
966
1338
|
const server = getHTTPServer(port, secure, options);
|
|
967
1339
|
|
|
968
|
-
if (
|
|
1340
|
+
if ((server as any)?.uws) {
|
|
1341
|
+
// uWS-backed port (HARPER_UWS_HTTP): uWS owns the socket, so route upgrades through uWS's
|
|
1342
|
+
// native app.ws() rather than the Node ws.WebSocketServer + server 'upgrade' event. We wire a
|
|
1343
|
+
// wsHandler into the shared uwsServeConfig; createUwsServer registers app.ws() when it listens.
|
|
1344
|
+
const cfg = uwsServeConfigs[port];
|
|
1345
|
+
if (cfg && !cfg.wsHandler) {
|
|
1346
|
+
// Honor a configured WebSocket maxPayload on the uWS transport too (else it defaults to 100 MiB).
|
|
1347
|
+
if (options.maxPayload != null) cfg.wsMaxPayload = options.maxPayload;
|
|
1348
|
+
cfg.wsHandler = (ws: any, upgrade: any) => {
|
|
1349
|
+
try {
|
|
1350
|
+
const request: any = new UwsRequest({
|
|
1351
|
+
method: 'GET',
|
|
1352
|
+
url: upgrade.url,
|
|
1353
|
+
headers: upgrade.headers,
|
|
1354
|
+
secure,
|
|
1355
|
+
ip: upgrade.ip,
|
|
1356
|
+
});
|
|
1357
|
+
request.isWebSocket = true;
|
|
1358
|
+
const chainCompletion = httpChain[port](request);
|
|
1359
|
+
websocketChains[port](ws, request, chainCompletion);
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
harperLogger.warn('Error in handling WS connection', error);
|
|
1362
|
+
try {
|
|
1363
|
+
ws.close();
|
|
1364
|
+
} catch {}
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
} else if (!websocketServers[port]) {
|
|
969
1369
|
websocketServers[port] = new WebSocketServer({
|
|
970
1370
|
noServer: true,
|
|
971
1371
|
// TODO: this should be a global config and not per ws listener
|
|
@@ -1023,7 +1423,7 @@ function onWebSocket(listener: (ws: WebSocket) => void, options: OnWebSocketOpti
|
|
|
1023
1423
|
host: options?.host || undefined,
|
|
1024
1424
|
};
|
|
1025
1425
|
websocketListeners[options?.runFirst ? 'unshift' : 'push'](wsEntry);
|
|
1026
|
-
websocketChains[port] = makeCallbackChain(websocketListeners, port, 1);
|
|
1426
|
+
websocketChains[port] = makeCallbackChain(websocketListeners, port, 1, 'websocket');
|
|
1027
1427
|
|
|
1028
1428
|
// mqtt doesn't invoke the http handler so this needs to be here to load up the http chains.
|
|
1029
1429
|
httpChain[port] = makeCallbackChain(httpResponders, port);
|
|
@@ -1105,6 +1505,92 @@ export function enableProxyProtocol(httpServer) {
|
|
|
1105
1505
|
});
|
|
1106
1506
|
}
|
|
1107
1507
|
|
|
1508
|
+
/**
|
|
1509
|
+
* Front a cleartext HTTP/2 server with optional PROXY v1 handling on a Unix domain socket.
|
|
1510
|
+
*
|
|
1511
|
+
* enableProxyProtocol() can't be used here: Node's Http2Session consumes the socket's
|
|
1512
|
+
* native handle directly, so data never surfaces as JS 'data' events to intercept. The
|
|
1513
|
+
* PROXY header must instead be consumed *before* the socket is handed to the HTTP/2
|
|
1514
|
+
* server. Bytes beyond the header (typically the coalesced h2 connection preface) are
|
|
1515
|
+
* unshifted back onto the socket; the native session picks them up (verified on Node 24
|
|
1516
|
+
* — covered by a unit test so a Node upgrade regressing this fails loudly).
|
|
1517
|
+
*
|
|
1518
|
+
* The returned server's close() also gracefully closes live h2 sessions (GOAWAY,
|
|
1519
|
+
* in-flight streams finish) so closeServers()'s generic server.close() drains instead
|
|
1520
|
+
* of riding its 5s force-exit backstop — the h1 mirror gets this via http.Server's
|
|
1521
|
+
* closeIdleConnections drain, which a net.Server doesn't have.
|
|
1522
|
+
*/
|
|
1523
|
+
export function createH2CProxyFront(h2Server, prehandoffTimeout = 10_000) {
|
|
1524
|
+
const sessions = new Set<any>();
|
|
1525
|
+
const prehandoffSockets = new Set<any>();
|
|
1526
|
+
let closing = false;
|
|
1527
|
+
h2Server.on('session', (session) => {
|
|
1528
|
+
// A connection can be mid-handoff (header read, session not yet created) when
|
|
1529
|
+
// close() runs — its session forms after the close sweep, so close it here or
|
|
1530
|
+
// it would never receive GOAWAY and would ride the 5s force-exit backstop.
|
|
1531
|
+
if (closing) session.close();
|
|
1532
|
+
sessions.add(session);
|
|
1533
|
+
session.on('close', () => sessions.delete(session));
|
|
1534
|
+
});
|
|
1535
|
+
const front = createNetServer({ noDelay: true }, (socket) => {
|
|
1536
|
+
let buf: Buffer | null = null;
|
|
1537
|
+
// Until handoff the h2 session's own handlers aren't attached yet: swallow socket
|
|
1538
|
+
// errors (a reset mid-header) and bound how long we'll wait for the header, so a
|
|
1539
|
+
// stalled connection can't hold an fd forever.
|
|
1540
|
+
prehandoffSockets.add(socket);
|
|
1541
|
+
socket.on('close', () => prehandoffSockets.delete(socket));
|
|
1542
|
+
const onPrehandoffError = () => socket.destroy();
|
|
1543
|
+
socket.on('error', onPrehandoffError);
|
|
1544
|
+
const onPrehandoffTimeout = () => socket.destroy();
|
|
1545
|
+
socket.setTimeout(prehandoffTimeout, onPrehandoffTimeout);
|
|
1546
|
+
const handoff = (rest: Buffer) => {
|
|
1547
|
+
prehandoffSockets.delete(socket);
|
|
1548
|
+
socket.removeListener('readable', onReadable);
|
|
1549
|
+
socket.removeListener('error', onPrehandoffError);
|
|
1550
|
+
socket.setTimeout(0);
|
|
1551
|
+
socket.removeListener('timeout', onPrehandoffTimeout);
|
|
1552
|
+
if (rest.length > 0) socket.unshift(rest);
|
|
1553
|
+
h2Server.emit('connection', socket);
|
|
1554
|
+
};
|
|
1555
|
+
const onReadable = () => {
|
|
1556
|
+
let chunk: Buffer;
|
|
1557
|
+
while ((chunk = socket.read()) !== null) {
|
|
1558
|
+
buf = buf ? Buffer.concat([buf, chunk]) : chunk;
|
|
1559
|
+
// Compare against "PROXY " for as many bytes as we have so far; a non-PROXY
|
|
1560
|
+
// prefix (e.g. a direct h2 client with no fronting proxy) is handed off as-is.
|
|
1561
|
+
const cmpLen = Math.min(PROXY_V1_PREFIX.length, buf.length);
|
|
1562
|
+
if (buf.compare(PROXY_V1_PREFIX, 0, cmpLen, 0, cmpLen) !== 0) return handoff(buf);
|
|
1563
|
+
const eol = buf.indexOf('\r\n');
|
|
1564
|
+
if (eol !== -1) {
|
|
1565
|
+
// Complete header: "PROXY TCP4 <src-ip> <dst-ip> <src-port> <dst-port>"
|
|
1566
|
+
const parts = buf.toString('latin1', 0, eol).split(' ');
|
|
1567
|
+
if (parts.length === 6) {
|
|
1568
|
+
// Override the UDS socket's undefined remoteAddress/remotePort with the real
|
|
1569
|
+
// client values; http2's compat req.socket proxies through to these.
|
|
1570
|
+
Object.defineProperty(socket, 'remoteAddress', { value: parts[2], configurable: true });
|
|
1571
|
+
Object.defineProperty(socket, 'remotePort', { value: parseInt(parts[4], 10), configurable: true });
|
|
1572
|
+
}
|
|
1573
|
+
return handoff(buf.subarray(eol + 2));
|
|
1574
|
+
}
|
|
1575
|
+
// No CRLF within the spec max — not a valid PROXY header after all.
|
|
1576
|
+
if (buf.length >= PROXY_V1_MAX_HEADER) return handoff(buf);
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
socket.on('readable', onReadable);
|
|
1580
|
+
});
|
|
1581
|
+
const netClose = front.close.bind(front);
|
|
1582
|
+
front.close = (callback?: (error?: Error) => void) => {
|
|
1583
|
+
closing = true;
|
|
1584
|
+
for (const session of sessions) session.close();
|
|
1585
|
+
// Header-waiting sockets carry no in-flight work; drop them so they can't hold
|
|
1586
|
+
// the close callback open for the rest of the pre-handoff timeout.
|
|
1587
|
+
for (const socket of prehandoffSockets) socket.destroy();
|
|
1588
|
+
h2Server.close();
|
|
1589
|
+
return netClose(callback);
|
|
1590
|
+
};
|
|
1591
|
+
return front;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1108
1594
|
function defaultNotFound(request, response) {
|
|
1109
1595
|
if (response.headersSent || response.writableEnded) return;
|
|
1110
1596
|
response.writeHead(404);
|
|
@@ -1113,7 +1599,7 @@ function defaultNotFound(request, response) {
|
|
|
1113
1599
|
}
|
|
1114
1600
|
let httpLogger: any;
|
|
1115
1601
|
|
|
1116
|
-
function
|
|
1602
|
+
function logHttpRequest(request: any, status: number, requestId: number, executionTime?: number) {
|
|
1117
1603
|
const logging = httpOptions.logging;
|
|
1118
1604
|
if (logging) {
|
|
1119
1605
|
if (!httpLogger) {
|