# Copyright 2023 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging from typing import Dict, Optional from zope.interface import implementer from twisted.internet import defer from twisted.internet.endpoints import ( HostnameEndpoint, UNIXClientEndpoint, wrapClientTLS, ) from twisted.internet.interfaces import IStreamClientEndpoint from twisted.python.failure import Failure from twisted.web.client import URI, HTTPConnectionPool, _AgentBase from twisted.web.error import SchemeNotSupported from twisted.web.http_headers import Headers from twisted.web.iweb import ( IAgent, IAgentEndpointFactory, IBodyProducer, IPolicyForHTTPS, IResponse, ) from synapse.config.workers import ( InstanceLocationConfig, InstanceTcpLocationConfig, InstanceUnixLocationConfig, ) from synapse.types import ISynapseReactor logger = logging.getLogger(__name__) @implementer(IAgentEndpointFactory) class ReplicationEndpointFactory: """Connect to a given TCP or UNIX socket""" def __init__( self, reactor: ISynapseReactor, instance_map: Dict[str, InstanceLocationConfig], context_factory: IPolicyForHTTPS, ) -> None: self.reactor = reactor self.instance_map = instance_map self.context_factory = context_factory def endpointForURI(self, uri: URI) -> IStreamClientEndpoint: """ This part of the factory decides what kind of endpoint is being connected to. Args: uri: The pre-parsed URI object containing all the uri data Returns: The correct client endpoint object """ # The given URI has a special scheme and includes the worker name. The # actual connection details are pulled from the instance map. worker_name = uri.netloc.decode("utf-8") location_config = self.instance_map[worker_name] scheme = location_config.scheme() if isinstance(location_config, InstanceTcpLocationConfig): endpoint = HostnameEndpoint( self.reactor, location_config.host, location_config.port, ) if scheme == "https": endpoint = wrapClientTLS( # The 'port' argument below isn't actually used by the function self.context_factory.creatorForNetloc( location_config.host.encode("utf-8"), location_config.port, ), endpoint, ) return endpoint elif isinstance(location_config, InstanceUnixLocationConfig): return UNIXClientEndpoint(self.reactor, location_config.path) else: raise SchemeNotSupported(f"Unsupported scheme: {scheme}") @implementer(IAgent) class ReplicationAgent(_AgentBase): """ Client for connecting to replication endpoints via HTTP and HTTPS. Much of this code is copied from Twisted's twisted.web.client.Agent. """ def __init__( self, reactor: ISynapseReactor, instance_map: Dict[str, InstanceLocationConfig], contextFactory: IPolicyForHTTPS, connectTimeout: Optional[float] = None, bindAddress: Optional[bytes] = None, pool: Optional[HTTPConnectionPool] = None, ): """ Create a ReplicationAgent. Args: reactor: A reactor for this Agent to place outgoing connections. contextFactory: A factory for TLS contexts, to control the verification parameters of OpenSSL. The default is to use a BrowserLikePolicyForHTTPS, so unless you have special requirements you can leave this as-is. connectTimeout: The amount of time that this Agent will wait for the peer to accept a connection. bindAddress: The local address for client sockets to bind to. pool: An HTTPConnectionPool instance, or None, in which case a non-persistent HTTPConnectionPool instance will be created. """ _AgentBase.__init__(self, reactor, pool) endpoint_factory = ReplicationEndpointFactory( reactor, instance_map, contextFactory ) self._endpointFactory = endpoint_factory def request( self, method: bytes, uri: bytes, headers: Optional[Headers] = None, bodyProducer: Optional[IBodyProducer] = None, ) -> "defer.Deferred[IResponse]": """ Issue a request to the server indicated by the given uri. An existing connection from the connection pool may be used or a new one may be created. Currently, HTTP, HTTPS and UNIX schemes are supported in uri. This is copied from twisted.web.client.Agent, except: * It uses a different pool key (combining the scheme with either host & port or socket path). * It does not call _ensureValidURI(...) as the strictness of IDNA2008 is not required when using a worker's name as a 'hostname' for Synapse HTTP Replication machinery. Specifically, this allows a range of ascii characters such as '+' and '_' in hostnames/worker's names. See: twisted.web.iweb.IAgent.request """ parsedURI = URI.fromBytes(uri) try: endpoint = self._endpointFactory.endpointForURI(parsedURI) except SchemeNotSupported: return defer.fail(Failure()) worker_name = parsedURI.netloc.decode("utf-8") key_scheme = self._endpointFactory.instance_map[worker_name].scheme() key_netloc = self._endpointFactory.instance_map[worker_name].netloc() # This sets the Pool key to be: # (http(s), ) or (unix, ) key = (key_scheme, key_netloc) # _requestWithEndpoint comes from _AgentBase class return self._requestWithEndpoint( key, endpoint, method, parsedURI, headers, bodyProducer, parsedURI.originForm, )