vortexpy 6.3.0__tar.gz → 6.4.0__tar.gz
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.
- {vortexpy-6.3.0 → vortexpy-6.4.0}/PKG-INFO +1 -1
- {vortexpy-6.3.0 → vortexpy-6.4.0}/pyproject.toml +1 -1
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/DeferUtil.py +16 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/MtlsServerSslOptions.py +76 -43
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexFactory.py +16 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexServerTcp.py +82 -5
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexServerTcp_test.py +56 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexServerWebsocket.py +75 -4
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexServerWebsocket_test.py +75 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexUtil.py +105 -1
- vortexpy-6.4.0/vortex/VortexUtil_test.py +292 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/__init__.py +1 -1
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleDataObservableHandler.py +34 -5
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleDataObservableHandler_test.py +43 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleDataObservableProxyHandler.py +13 -5
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/twisted_runtime_metrics.py +116 -54
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/twisted_runtime_metrics_test.py +34 -9
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortexpy.egg-info/PKG-INFO +1 -1
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortexpy.egg-info/SOURCES.txt +1 -1
- vortexpy-6.3.0/LICENSE +0 -22
- {vortexpy-6.3.0 → vortexpy-6.4.0}/README.md +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/example/TwistedDeferredTimeout.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/example/__init__.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/example/run_http_upgraded_websocket_ping_client.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/example/run_http_upgraded_websocket_ping_server.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/example/run_http_upgraded_websocket_rpc_test_client.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/example/run_http_upgraded_websocket_rpc_test_server.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/example/run_websocket_rpc_test_client.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/example/run_websocket_rpc_test_server.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/example/setup_example_logging.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/example/store_and_load_json.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/setup.cfg +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/DataWrapTuple.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/DeferUtil_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/Jsonable.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/Jsonable_tuple_dict_coercion_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/MtlsAuthorizedClientRegistry.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/MtlsAuthorizedClientRegistry_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/Payload.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/PayloadEndpoint.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/PayloadEndpointTest.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/PayloadEnvelope.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/PayloadFilterKeys.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/PayloadIO.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/PayloadPriority.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/PayloadResponse.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/PayloadResponseTest.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/PayloadResponse_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/PayloadTest.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/PerformTestActionTuple.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/SerialiseUtil.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/SerialiseUtil_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/TestTuple.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/Tuple.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/TupleAction.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/TupleBson.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/TupleBson_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/TupleDataForTest.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/TupleDocumentConverter.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/TuplePerfTest.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/TupleRestful.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/TupleRestful_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/TupleSelector.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/TupleTest.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexABC.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexAccessControlContextABC.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexAuthenticatorABC.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexClientHttp.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexClientHttpTest.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexClientTcp.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexClientWebsocketFactory.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexConnectionABC.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexEndpointPermission.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexFactory_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexPayloadProtocol.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexPermission.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexPermissionError.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexServer.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexServerConnection.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexServerConnection_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexServerHttpResource.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexServer_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexSession.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexSession_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexTest.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexUserDetailsABC.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexWritePushProducer.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/VortexWritePushProducer_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/data_loader/TupleDataLiveLockManager_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/data_loader/TupleDataLoader.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/data_loader/__init__.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/event_bus/TupleChangeEventABC.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/event_bus/TupleChangeEventBus.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/event_bus/__init__.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleActionProcessor.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleActionProcessorProxy.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleActionProcessor_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleActionPushOffline.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleActionPushOffline_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleDataActionClient.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleDataActionClient_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleDataObservableCache.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleDataObservableCache_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleDataObservableProxyHandler_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleDataObserverClient.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleDataOfflineObserverService.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleDataOfflineObserverService_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/TupleDataProviderABC.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/handler/__init__.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/mypy_plugin.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/payload_many_tuples_benchmark_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/py.typed +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/restful/GzippedDataHttpClient.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/restful/RestfulHttpClient.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/restful/RestfulResource.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/restful/__init__.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/restful/restful_http_client_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/restful/restful_resource_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/rpc/RPC.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/rpc/RPCTest.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/rpc/RPC_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/rpc/__init__.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/sqla_orm/VortexTupleJson.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/sqla_orm/__init__.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/storage/TupleActionStorageSqlite.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/storage/TupleOfflineStorageNameService.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/storage/TupleOfflineStorageSecureService.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/storage/TupleOfflineStorageSecureService_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/storage/TupleOfflineStorageService.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/storage/TupleStorageServiceABC.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/storage/TupleStorageSqlite.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/storage/TupleStorageSqliteService.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/storage/TupleStorageSqliteService_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/storage/__init__.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/tuple_json_schema/__init__.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/tuple_json_schema/constants.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortex/tuple_test_fixtures.py +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortexpy.egg-info/dependency_links.txt +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortexpy.egg-info/entry_points.txt +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortexpy.egg-info/requires.txt +0 -0
- {vortexpy-6.3.0 → vortexpy-6.4.0}/vortexpy.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vortexpy"
|
|
7
|
-
version = "6.
|
|
7
|
+
version = "6.4.0"
|
|
8
8
|
license = {text = "MIT"}
|
|
9
9
|
authors = [{name = "Flying Ion Pty Ltd", email = "contact@flyingion.com"}]
|
|
10
10
|
description = "Flying Ion's observable, routable, data serialisation and transport code."
|
|
@@ -16,6 +16,7 @@ import twisted
|
|
|
16
16
|
from twisted.internet import reactor
|
|
17
17
|
from twisted.internet.defer import (
|
|
18
18
|
maybeDeferred,
|
|
19
|
+
CancelledError,
|
|
19
20
|
Deferred,
|
|
20
21
|
inlineCallbacks,
|
|
21
22
|
ensureDeferred,
|
|
@@ -239,6 +240,17 @@ def deferToThreadWrapWithLogger(
|
|
|
239
240
|
)
|
|
240
241
|
|
|
241
242
|
def handleFailure(failure):
|
|
243
|
+
if failure.check(CancelledError):
|
|
244
|
+
# Cancellation is designed control flow (for example a
|
|
245
|
+
# limitConcurrency timeout quarantining the thread), not
|
|
246
|
+
# a threaded call failure.
|
|
247
|
+
loggerArg.debug(
|
|
248
|
+
"Threaded call cancelled in %s.%s",
|
|
249
|
+
funcToWrap.__module__,
|
|
250
|
+
funcToWrap.__qualname__,
|
|
251
|
+
)
|
|
252
|
+
return True if consumeError else failure
|
|
253
|
+
|
|
242
254
|
if logException:
|
|
243
255
|
return vortexLogFailure(
|
|
244
256
|
failure, loggerArg, consumeError=consumeError
|
|
@@ -253,6 +265,10 @@ def deferToThreadWrapWithLogger(
|
|
|
253
265
|
return True if consumeError else failure
|
|
254
266
|
|
|
255
267
|
d: Deferred[_R] = deferToThread(timedFuncToRun, *args, **kwargs)
|
|
268
|
+
# Marks this Deferred as threaded for limitConcurrency: a timed
|
|
269
|
+
# out threaded call cannot be killed, so the limiter accumulates
|
|
270
|
+
# marked Deferreds in its quarantine registry.
|
|
271
|
+
d._isVortexThreadedCall = True # type: ignore[attr-defined]
|
|
256
272
|
d.addCallback(checkForInlineCallbackYield)
|
|
257
273
|
d.addErrback(handleFailure)
|
|
258
274
|
return d
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import logging
|
|
14
|
-
from
|
|
14
|
+
from collections.abc import Callable
|
|
15
15
|
|
|
16
16
|
from OpenSSL import SSL, crypto
|
|
17
17
|
from twisted.internet._sslverify import Certificate, KeyPair, TLSVersion
|
|
@@ -31,9 +31,20 @@ class _MtlsServerCertificateOptions(CertificateOptions):
|
|
|
31
31
|
self,
|
|
32
32
|
*,
|
|
33
33
|
authorized_client_registry: MtlsAuthorizedClientRegistry,
|
|
34
|
+
peer_cert_acceptance_hook: Callable[[str], bool] | None = None,
|
|
34
35
|
**kwargs,
|
|
35
36
|
) -> None:
|
|
37
|
+
"""Constructor
|
|
38
|
+
|
|
39
|
+
:param authorized_client_registry: Mutable authorized client public
|
|
40
|
+
keys consulted by the verify callback.
|
|
41
|
+
:param peer_cert_acceptance_hook: Optional acceptance hook receiving
|
|
42
|
+
the peer leaf certificate PEM; consulted only when OpenSSL has
|
|
43
|
+
already validated the chain (``preverify_ok == 1``) and the
|
|
44
|
+
registry does not authorize the peer public key.
|
|
45
|
+
"""
|
|
36
46
|
self._authorizedClientRegistry = authorized_client_registry
|
|
47
|
+
self._peerCertAcceptanceHook = peer_cert_acceptance_hook
|
|
37
48
|
super().__init__(**kwargs)
|
|
38
49
|
|
|
39
50
|
def _makeContext(self):
|
|
@@ -57,47 +68,7 @@ class _MtlsServerCertificateOptions(CertificateOptions):
|
|
|
57
68
|
verify_flags |= SSL.VERIFY_CLIENT_ONCE
|
|
58
69
|
self.trustRoot._addCACertsToContext(ctx)
|
|
59
70
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def verifyCallback(
|
|
63
|
-
conn: SSL.Connection,
|
|
64
|
-
cert: crypto.X509,
|
|
65
|
-
errno: int,
|
|
66
|
-
depth: int,
|
|
67
|
-
preverify_ok: int,
|
|
68
|
-
) -> int:
|
|
69
|
-
fail = 0
|
|
70
|
-
|
|
71
|
-
if depth != 0:
|
|
72
|
-
return preverify_ok
|
|
73
|
-
|
|
74
|
-
if preverify_ok == fail:
|
|
75
|
-
return preverify_ok
|
|
76
|
-
|
|
77
|
-
session_peer_public_key_bytes = crypto.dump_publickey(
|
|
78
|
-
crypto.FILETYPE_ASN1, cert.get_pubkey()
|
|
79
|
-
)
|
|
80
|
-
if not registry.isPublicKeyAuthorized(session_peer_public_key_bytes):
|
|
81
|
-
subject = cert.get_subject()
|
|
82
|
-
logger.error(
|
|
83
|
-
"mTLS peer verify failed: certificate not authorized subject=%s",
|
|
84
|
-
subject,
|
|
85
|
-
)
|
|
86
|
-
return fail
|
|
87
|
-
|
|
88
|
-
identity = registry.getIdentityForPublicKey(
|
|
89
|
-
session_peer_public_key_bytes
|
|
90
|
-
)
|
|
91
|
-
if identity is not None:
|
|
92
|
-
logger.info(
|
|
93
|
-
"mTLS peer verified instanceId=%s owner=%s fingerprint=%s",
|
|
94
|
-
identity.instanceId or "(unknown)",
|
|
95
|
-
identity.ownerUsername or "(unknown)",
|
|
96
|
-
identity.fingerprint,
|
|
97
|
-
)
|
|
98
|
-
return preverify_ok
|
|
99
|
-
|
|
100
|
-
ctx.set_verify(verify_flags, verifyCallback)
|
|
71
|
+
ctx.set_verify(verify_flags, self._verifyPeer)
|
|
101
72
|
if self.verifyDepth is not None:
|
|
102
73
|
ctx.set_verify_depth(self.verifyDepth)
|
|
103
74
|
|
|
@@ -124,14 +95,70 @@ class _MtlsServerCertificateOptions(CertificateOptions):
|
|
|
124
95
|
|
|
125
96
|
return ctx
|
|
126
97
|
|
|
98
|
+
def _verifyPeer(
|
|
99
|
+
self,
|
|
100
|
+
conn: SSL.Connection,
|
|
101
|
+
cert: crypto.X509,
|
|
102
|
+
errno: int,
|
|
103
|
+
depth: int,
|
|
104
|
+
preverify_ok: int,
|
|
105
|
+
) -> int:
|
|
106
|
+
"""OpenSSL verify callback for the registry-backed mTLS allowlist.
|
|
107
|
+
|
|
108
|
+
OpenSSL validates the chain (signatures, validity, path length)
|
|
109
|
+
before this callback reports the leaf at depth 0. A
|
|
110
|
+
``preverify_ok == 0`` leaf is returned unchanged so expiry and
|
|
111
|
+
path-length violations are never bypassed by the acceptance hook.
|
|
112
|
+
"""
|
|
113
|
+
fail = 0
|
|
114
|
+
|
|
115
|
+
if depth != 0:
|
|
116
|
+
return preverify_ok
|
|
117
|
+
|
|
118
|
+
if preverify_ok == fail:
|
|
119
|
+
return preverify_ok
|
|
120
|
+
|
|
121
|
+
registry = self._authorizedClientRegistry
|
|
122
|
+
session_peer_public_key_bytes = crypto.dump_publickey(
|
|
123
|
+
crypto.FILETYPE_ASN1, cert.get_pubkey()
|
|
124
|
+
)
|
|
125
|
+
if not registry.isPublicKeyAuthorized(session_peer_public_key_bytes):
|
|
126
|
+
acceptanceHook = self._peerCertAcceptanceHook
|
|
127
|
+
if acceptanceHook is not None and acceptanceHook(
|
|
128
|
+
crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("ascii")
|
|
129
|
+
):
|
|
130
|
+
logger.info(
|
|
131
|
+
"mTLS peer verified via peer certificate"
|
|
132
|
+
" acceptance hook subject=%s",
|
|
133
|
+
cert.get_subject(),
|
|
134
|
+
)
|
|
135
|
+
return preverify_ok
|
|
136
|
+
subject = cert.get_subject()
|
|
137
|
+
logger.error(
|
|
138
|
+
"mTLS peer verify failed: certificate not authorized subject=%s",
|
|
139
|
+
subject,
|
|
140
|
+
)
|
|
141
|
+
return fail
|
|
142
|
+
|
|
143
|
+
identity = registry.getIdentityForPublicKey(session_peer_public_key_bytes)
|
|
144
|
+
if identity is not None:
|
|
145
|
+
logger.info(
|
|
146
|
+
"mTLS peer verified instanceId=%s owner=%s fingerprint=%s",
|
|
147
|
+
identity.instanceId or "(unknown)",
|
|
148
|
+
identity.ownerUsername or "(unknown)",
|
|
149
|
+
identity.fingerprint,
|
|
150
|
+
)
|
|
151
|
+
return preverify_ok
|
|
152
|
+
|
|
127
153
|
|
|
128
154
|
def buildMtlsServerCertificateOptions(
|
|
129
155
|
private_key_with_full_chain: PrivateKeyWithFullChain,
|
|
130
156
|
trust_root,
|
|
131
157
|
authorized_client_registry: MtlsAuthorizedClientRegistry,
|
|
132
158
|
*,
|
|
133
|
-
acceptable_protocols:
|
|
159
|
+
acceptable_protocols: list[bytes] | None = None,
|
|
134
160
|
dh_parameters=None,
|
|
161
|
+
peer_cert_acceptance_hook: Callable[[str], bool] | None = None,
|
|
135
162
|
) -> CertificateOptions:
|
|
136
163
|
"""Build server TLS options with dynamic authorized-client verification.
|
|
137
164
|
|
|
@@ -142,6 +169,11 @@ def buildMtlsServerCertificateOptions(
|
|
|
142
169
|
:param private_key_with_full_chain: Server key + certificate chain PEM bank.
|
|
143
170
|
:param trust_root: Twisted trust root for client certificate chain validation.
|
|
144
171
|
:param authorized_client_registry: Mutable authorized client public keys.
|
|
172
|
+
:param peer_cert_acceptance_hook: Optional acceptance hook receiving the
|
|
173
|
+
peer leaf certificate PEM, consulted only when OpenSSL has validated
|
|
174
|
+
the chain and the registry does not authorize the peer public key;
|
|
175
|
+
it adds constraints OpenSSL cannot know (pinned-issuer membership,
|
|
176
|
+
certificate-name policy).
|
|
145
177
|
:return: Twisted ``CertificateOptions`` for ``reactor.listenSSL``.
|
|
146
178
|
"""
|
|
147
179
|
if acceptable_protocols is None:
|
|
@@ -165,6 +197,7 @@ def buildMtlsServerCertificateOptions(
|
|
|
165
197
|
|
|
166
198
|
return _MtlsServerCertificateOptions(
|
|
167
199
|
authorized_client_registry=authorized_client_registry,
|
|
200
|
+
peer_cert_acceptance_hook=peer_cert_acceptance_hook,
|
|
168
201
|
privateKey=twisted_loaded_chain.privateKey,
|
|
169
202
|
certificate=twisted_loaded_chain.certificate,
|
|
170
203
|
extraCertChain=twisted_loaded_chain.intermediateCAs,
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
12
|
from collections import defaultdict, namedtuple
|
|
13
|
+
from collections.abc import Callable
|
|
13
14
|
from datetime import datetime
|
|
14
15
|
|
|
15
16
|
import pytz
|
|
@@ -581,6 +582,7 @@ class VortexFactory:
|
|
|
581
582
|
cls,
|
|
582
583
|
name: str,
|
|
583
584
|
mtlsAuthorizedClientRegistry: MtlsAuthorizedClientRegistry | None = None,
|
|
585
|
+
peer_cert_acceptance_hook: Callable[[str], bool] | None = None,
|
|
584
586
|
) -> VortexWebSocketUpgradeResource:
|
|
585
587
|
"""Create Http Websocket Resource
|
|
586
588
|
|
|
@@ -588,6 +590,9 @@ class VortexFactory:
|
|
|
588
590
|
:param mtlsAuthorizedClientRegistry: Authorized mTLS client
|
|
589
591
|
certificate registry forwarded to the upgrade resource;
|
|
590
592
|
``None`` when the listener is not mTLS.
|
|
593
|
+
:param peer_cert_acceptance_hook: Optional acceptance hook
|
|
594
|
+
forwarded to the upgrade resource, consulted only when the
|
|
595
|
+
registry does not authorize the peer public key.
|
|
591
596
|
:return: The configured :class:`VortexWebSocketUpgradeResource`.
|
|
592
597
|
"""
|
|
593
598
|
vortexServer = VortexServer(name, requiresBase64Encoding=False)
|
|
@@ -599,6 +604,7 @@ class VortexFactory:
|
|
|
599
604
|
return VortexWebSocketUpgradeResource(
|
|
600
605
|
websocketFactory,
|
|
601
606
|
mtlsAuthorizedClientRegistry=mtlsAuthorizedClientRegistry,
|
|
607
|
+
peer_cert_acceptance_hook=peer_cert_acceptance_hook,
|
|
602
608
|
)
|
|
603
609
|
|
|
604
610
|
@classmethod
|
|
@@ -607,6 +613,7 @@ class VortexFactory:
|
|
|
607
613
|
name: str,
|
|
608
614
|
rootResource,
|
|
609
615
|
mtlsAuthorizedClientRegistry: MtlsAuthorizedClientRegistry | None = None,
|
|
616
|
+
peer_cert_acceptance_hook: Callable[[str], bool] | None = None,
|
|
610
617
|
) -> None:
|
|
611
618
|
"""Create Server
|
|
612
619
|
|
|
@@ -620,6 +627,9 @@ class VortexFactory:
|
|
|
620
627
|
:param mtlsAuthorizedClientRegistry: Authorized mTLS client
|
|
621
628
|
certificate registry forwarded to the upgrade resource;
|
|
622
629
|
``None`` when the listener is not mTLS.
|
|
630
|
+
:param peer_cert_acceptance_hook: Optional acceptance hook
|
|
631
|
+
forwarded to the upgrade resource, consulted only when the
|
|
632
|
+
registry does not authorize the peer public key.
|
|
623
633
|
:return: None
|
|
624
634
|
"""
|
|
625
635
|
|
|
@@ -631,6 +641,7 @@ class VortexFactory:
|
|
|
631
641
|
websocketResource = VortexWebSocketUpgradeResource(
|
|
632
642
|
websocketFactory,
|
|
633
643
|
mtlsAuthorizedClientRegistry=mtlsAuthorizedClientRegistry,
|
|
644
|
+
peer_cert_acceptance_hook=peer_cert_acceptance_hook,
|
|
634
645
|
)
|
|
635
646
|
rootResource.putChild(b"vortexws", websocketResource)
|
|
636
647
|
|
|
@@ -640,6 +651,7 @@ class VortexFactory:
|
|
|
640
651
|
name: str,
|
|
641
652
|
port: int,
|
|
642
653
|
mtlsAuthorizedClientRegistry: MtlsAuthorizedClientRegistry | None = None,
|
|
654
|
+
peer_cert_acceptance_hook: Callable[[str], bool] | None = None,
|
|
643
655
|
) -> None:
|
|
644
656
|
"""Create Server
|
|
645
657
|
|
|
@@ -653,6 +665,9 @@ class VortexFactory:
|
|
|
653
665
|
:param mtlsAuthorizedClientRegistry: Authorized mTLS client
|
|
654
666
|
certificate registry forwarded to the TCP server factory;
|
|
655
667
|
``None`` when the listener is not mTLS.
|
|
668
|
+
:param peer_cert_acceptance_hook: Optional acceptance hook
|
|
669
|
+
forwarded to the TCP server factory, consulted only when the
|
|
670
|
+
registry does not authorize the peer public key.
|
|
656
671
|
:return: None
|
|
657
672
|
"""
|
|
658
673
|
|
|
@@ -661,6 +676,7 @@ class VortexFactory:
|
|
|
661
676
|
vortexTcpServerFactory = VortexTcpServerFactory(
|
|
662
677
|
vortexServer,
|
|
663
678
|
mtlsAuthorizedClientRegistry=mtlsAuthorizedClientRegistry,
|
|
679
|
+
peer_cert_acceptance_hook=peer_cert_acceptance_hook,
|
|
664
680
|
)
|
|
665
681
|
port = reactor.listenTCP(port, vortexTcpServerFactory)
|
|
666
682
|
cls.__listeningPorts.append(port)
|
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import hashlib
|
|
11
12
|
import logging
|
|
13
|
+
from collections.abc import Callable
|
|
12
14
|
|
|
13
15
|
from OpenSSL import crypto
|
|
14
16
|
from twisted.internet.defer import succeed
|
|
15
17
|
from twisted.internet.protocol import Factory, connectionDone
|
|
16
18
|
|
|
17
|
-
from vortex.MtlsAuthorizedClientRegistry import
|
|
19
|
+
from vortex.MtlsAuthorizedClientRegistry import (
|
|
20
|
+
AuthorizedClientIdentity,
|
|
21
|
+
MtlsAuthorizedClientRegistry,
|
|
22
|
+
)
|
|
18
23
|
from vortex.VortexPayloadProtocol import VortexPayloadProtocol
|
|
19
24
|
from vortex.VortexServerConnection import VortexServerConnection
|
|
20
25
|
from vortex.VortexSession import (
|
|
@@ -36,6 +41,7 @@ class VortexTcpServerProtocol(VortexPayloadProtocol):
|
|
|
36
41
|
vortex: VortexServer,
|
|
37
42
|
addr,
|
|
38
43
|
mtlsAuthorizedClientRegistry: MtlsAuthorizedClientRegistry | None = None,
|
|
44
|
+
peer_cert_acceptance_hook: Callable[[str], bool] | None = None,
|
|
39
45
|
) -> None:
|
|
40
46
|
"""Constructor
|
|
41
47
|
|
|
@@ -44,11 +50,17 @@ class VortexTcpServerProtocol(VortexPayloadProtocol):
|
|
|
44
50
|
:param mtlsAuthorizedClientRegistry: Authorized mTLS client
|
|
45
51
|
certificate registry for this listener; ``None`` when the
|
|
46
52
|
listener is not mTLS.
|
|
53
|
+
:param peer_cert_acceptance_hook: Optional acceptance hook
|
|
54
|
+
receiving the peer leaf certificate PEM, consulted only when
|
|
55
|
+
the registry does not authorize the peer public key; it adds
|
|
56
|
+
constraints OpenSSL cannot know (pinned-issuer membership,
|
|
57
|
+
certificate-name policy).
|
|
47
58
|
"""
|
|
48
59
|
VortexPayloadProtocol.__init__(self, logger)
|
|
49
60
|
self._vortex = vortex
|
|
50
61
|
self._addr = addr
|
|
51
62
|
self._mtlsAuthorizedClientRegistry = mtlsAuthorizedClientRegistry
|
|
63
|
+
self._peerCertAcceptanceHook = peer_cert_acceptance_hook
|
|
52
64
|
|
|
53
65
|
self._conn = None
|
|
54
66
|
|
|
@@ -94,10 +106,14 @@ class VortexTcpServerProtocol(VortexPayloadProtocol):
|
|
|
94
106
|
When an authorized-client registry is present and the TLS
|
|
95
107
|
transport carries a peer certificate, the peer public key is
|
|
96
108
|
resolved to its :class:`AuthorizedClientIdentity` and stored on
|
|
97
|
-
the session before :class:`PayloadIO` processes payloads.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
the session before :class:`PayloadIO` processes payloads. When
|
|
110
|
+
the registry does not hold the peer key, the peer certificate
|
|
111
|
+
acceptance hook decides identity resolution — an accepted peer
|
|
112
|
+
surfaces an identity derived from its certificate. The session
|
|
113
|
+
attribute stays ``None`` when no registry is passed, the
|
|
114
|
+
listener is not mTLS, or neither the registry nor the acceptance
|
|
115
|
+
hook authorizes the peer
|
|
116
|
+
(``_MtlsServerCertificateOptions._verifyPeer`` already rejects
|
|
101
117
|
unauthorized keys during the TLS handshake).
|
|
102
118
|
"""
|
|
103
119
|
if self._mtlsAuthorizedClientRegistry is None:
|
|
@@ -116,6 +132,10 @@ class VortexTcpServerProtocol(VortexPayloadProtocol):
|
|
|
116
132
|
identity = self._mtlsAuthorizedClientRegistry.getIdentityForPublicKey(
|
|
117
133
|
publicKeyAsn1
|
|
118
134
|
)
|
|
135
|
+
if identity is None:
|
|
136
|
+
identity = self._resolveIdentityFromAcceptanceHook(
|
|
137
|
+
peerCertificate, publicKeyAsn1
|
|
138
|
+
)
|
|
119
139
|
if identity is None:
|
|
120
140
|
logger.warning(
|
|
121
141
|
"mTLS vortex TCP peer public key is not authorized;"
|
|
@@ -133,6 +153,57 @@ class VortexTcpServerProtocol(VortexPayloadProtocol):
|
|
|
133
153
|
)
|
|
134
154
|
vortexSession.mtlsClientIdentity = identity
|
|
135
155
|
|
|
156
|
+
def _resolveIdentityFromAcceptanceHook(
|
|
157
|
+
self,
|
|
158
|
+
peerCertificate: crypto.X509,
|
|
159
|
+
publicKeyAsn1: bytes,
|
|
160
|
+
) -> AuthorizedClientIdentity | None:
|
|
161
|
+
"""Resolve an identity for a peer the acceptance hook accepts.
|
|
162
|
+
|
|
163
|
+
The registry authorizes hub-issued certificates by public key;
|
|
164
|
+
peers the TLS acceptance hook accepted (for example
|
|
165
|
+
broker-issued leaves chaining to a pinned intermediate) surface
|
|
166
|
+
an identity derived from their certificate — the common name
|
|
167
|
+
identifies the peer and the fingerprint supports audit logs.
|
|
168
|
+
|
|
169
|
+
:param peerCertificate: The peer leaf certificate from the TLS
|
|
170
|
+
transport
|
|
171
|
+
:param publicKeyAsn1: The peer public key, DER encoded
|
|
172
|
+
:return: The derived identity, or ``None`` when no hook is
|
|
173
|
+
wired or the hook rejects the peer
|
|
174
|
+
"""
|
|
175
|
+
acceptanceHook = self._peerCertAcceptanceHook
|
|
176
|
+
if acceptanceHook is None:
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
certificatePem = crypto.dump_certificate(
|
|
180
|
+
crypto.FILETYPE_PEM, peerCertificate
|
|
181
|
+
).decode("ascii")
|
|
182
|
+
if not acceptanceHook(certificatePem):
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
fingerprint = hashlib.sha256(
|
|
186
|
+
crypto.dump_certificate(crypto.FILETYPE_ASN1, peerCertificate)
|
|
187
|
+
).hexdigest()[:16]
|
|
188
|
+
subject = peerCertificate.get_subject()
|
|
189
|
+
commonName = (
|
|
190
|
+
subject.CN.decode("utf-8")
|
|
191
|
+
if isinstance(subject.CN, bytes)
|
|
192
|
+
else str(subject.CN or "")
|
|
193
|
+
)
|
|
194
|
+
logger.info(
|
|
195
|
+
"mTLS peer accepted via certificate acceptance hook cn=%s fingerprint=%s",
|
|
196
|
+
commonName,
|
|
197
|
+
fingerprint,
|
|
198
|
+
)
|
|
199
|
+
return AuthorizedClientIdentity(
|
|
200
|
+
fingerprint=fingerprint,
|
|
201
|
+
publicKeyAsn1=publicKeyAsn1,
|
|
202
|
+
instanceId=commonName,
|
|
203
|
+
ownerUsername="",
|
|
204
|
+
commonName=commonName,
|
|
205
|
+
)
|
|
206
|
+
|
|
136
207
|
def _deliverPayload(self, payloadEnvelope):
|
|
137
208
|
vs = (
|
|
138
209
|
self._httpSession.getComponent(IVortexSession)
|
|
@@ -186,6 +257,7 @@ class VortexTcpServerFactory(Factory):
|
|
|
186
257
|
self,
|
|
187
258
|
vortexServer: VortexServer,
|
|
188
259
|
mtlsAuthorizedClientRegistry: MtlsAuthorizedClientRegistry | None = None,
|
|
260
|
+
peer_cert_acceptance_hook: Callable[[str], bool] | None = None,
|
|
189
261
|
) -> None:
|
|
190
262
|
"""Constructor
|
|
191
263
|
|
|
@@ -194,9 +266,13 @@ class VortexTcpServerFactory(Factory):
|
|
|
194
266
|
:param mtlsAuthorizedClientRegistry: Authorized mTLS client
|
|
195
267
|
certificate registry for this listener; ``None`` when the
|
|
196
268
|
listener is not mTLS.
|
|
269
|
+
:param peer_cert_acceptance_hook: Optional acceptance hook
|
|
270
|
+
forwarded to built protocols, consulted only when the
|
|
271
|
+
registry does not authorize the peer public key.
|
|
197
272
|
"""
|
|
198
273
|
self._vortexServer = vortexServer
|
|
199
274
|
self._mtlsAuthorizedClientRegistry = mtlsAuthorizedClientRegistry
|
|
275
|
+
self._peerCertAcceptanceHook = peer_cert_acceptance_hook
|
|
200
276
|
|
|
201
277
|
def buildProtocol(self, addr):
|
|
202
278
|
from vortex.VortexFactory import VortexFactory
|
|
@@ -208,6 +284,7 @@ class VortexTcpServerFactory(Factory):
|
|
|
208
284
|
self._vortexServer,
|
|
209
285
|
addr,
|
|
210
286
|
mtlsAuthorizedClientRegistry=self._mtlsAuthorizedClientRegistry,
|
|
287
|
+
peer_cert_acceptance_hook=self._peerCertAcceptanceHook,
|
|
211
288
|
)
|
|
212
289
|
p.factory = self
|
|
213
290
|
return p
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
from collections.abc import Callable
|
|
13
14
|
from unittest.mock import MagicMock, patch
|
|
14
15
|
|
|
15
16
|
from OpenSSL import crypto
|
|
@@ -36,6 +37,7 @@ def _makeSelfSignedCert(commonName: bytes = b"agent-instance-1") -> crypto.X509:
|
|
|
36
37
|
def _makeProtocol(
|
|
37
38
|
registry: MtlsAuthorizedClientRegistry | None,
|
|
38
39
|
transport,
|
|
40
|
+
peerCertAcceptanceHook: Callable[[str], bool] | None = None,
|
|
39
41
|
) -> VortexTcpServerProtocol:
|
|
40
42
|
vortex = MagicMock()
|
|
41
43
|
vortex.uuid.return_value = "server-uuid"
|
|
@@ -44,6 +46,7 @@ def _makeProtocol(
|
|
|
44
46
|
vortex,
|
|
45
47
|
MagicMock(host="10.0.0.2"),
|
|
46
48
|
mtlsAuthorizedClientRegistry=registry,
|
|
49
|
+
peer_cert_acceptance_hook=peerCertAcceptanceHook,
|
|
47
50
|
)
|
|
48
51
|
protocol.transport = transport
|
|
49
52
|
return protocol
|
|
@@ -59,6 +62,20 @@ def test_factory_hands_registry_to_built_protocols() -> None:
|
|
|
59
62
|
assert protocol._mtlsAuthorizedClientRegistry is registry
|
|
60
63
|
|
|
61
64
|
|
|
65
|
+
def test_factory_hands_acceptance_hook_to_built_protocols() -> None:
|
|
66
|
+
def acceptingHook(leafCertPem: str) -> bool:
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
factory = VortexTcpServerFactory(
|
|
70
|
+
MagicMock(), peer_cert_acceptance_hook=acceptingHook
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
with patch("vortex.VortexFactory.VortexFactory.canConnect", return_value=True):
|
|
74
|
+
protocol = factory.buildProtocol(MagicMock(host="10.0.0.2"))
|
|
75
|
+
|
|
76
|
+
assert protocol._peerCertAcceptanceHook is acceptingHook
|
|
77
|
+
|
|
78
|
+
|
|
62
79
|
def test_factory_defaults_registry_to_none() -> None:
|
|
63
80
|
factory = VortexTcpServerFactory(MagicMock())
|
|
64
81
|
|
|
@@ -119,3 +136,42 @@ def test_mtls_identity_none_for_unauthorized_peer_key() -> None:
|
|
|
119
136
|
|
|
120
137
|
session = protocol._httpSession.getComponent(IVortexSession)
|
|
121
138
|
assert session.mtlsClientIdentity is None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_mtls_identity_resolved_for_hook_accepted_peer_cert() -> None:
|
|
142
|
+
registry = MtlsAuthorizedClientRegistry()
|
|
143
|
+
transport = MagicMock()
|
|
144
|
+
transport.getPeerCertificate.return_value = _makeSelfSignedCert(
|
|
145
|
+
b"user-mcp-operator@example.com"
|
|
146
|
+
)
|
|
147
|
+
protocol = _makeProtocol(registry, transport, lambda leafCertPem: True)
|
|
148
|
+
|
|
149
|
+
protocol._nameAndUuidReceived("remote-vortex", "remote-uuid")
|
|
150
|
+
|
|
151
|
+
session = protocol._httpSession.getComponent(IVortexSession)
|
|
152
|
+
assert session.mtlsClientIdentity is not None, (
|
|
153
|
+
"a hook-accepted peer must surface an identity on the session"
|
|
154
|
+
)
|
|
155
|
+
assert session.mtlsClientIdentity.commonName == "user-mcp-operator@example.com"
|
|
156
|
+
assert session.mtlsClientIdentity.instanceId == "user-mcp-operator@example.com", (
|
|
157
|
+
"the derived identity names the peer by its certificate common name"
|
|
158
|
+
)
|
|
159
|
+
assert len(session.mtlsClientIdentity.fingerprint) == 16, (
|
|
160
|
+
"the derived fingerprint mirrors the registry's sha256[:16] form"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_mtls_identity_none_when_hook_rejects() -> None:
|
|
165
|
+
registry = MtlsAuthorizedClientRegistry()
|
|
166
|
+
transport = MagicMock()
|
|
167
|
+
transport.getPeerCertificate.return_value = _makeSelfSignedCert(
|
|
168
|
+
b"user-mcp-operator@example.com"
|
|
169
|
+
)
|
|
170
|
+
protocol = _makeProtocol(registry, transport, lambda leafCertPem: False)
|
|
171
|
+
|
|
172
|
+
protocol._nameAndUuidReceived("remote-vortex", "remote-uuid")
|
|
173
|
+
|
|
174
|
+
session = protocol._httpSession.getComponent(IVortexSession)
|
|
175
|
+
assert session.mtlsClientIdentity is None, (
|
|
176
|
+
"a hook-rejected peer must carry no client identity"
|
|
177
|
+
)
|