summary refs log tree commit diff
diff options
context:
space:
mode:
authorAndrew Morgan <andrew@amorgan.xyz>2023-02-23 23:25:57 +0000
committerAndrew Morgan <andrew@amorgan.xyz>2023-02-23 23:40:07 +0000
commit29cc17fde7f7438ccf826abe282bfbf8403c0d26 (patch)
treeaebce3a91a8dbe329e66d8c9588f6f7b0101a209
parentFix typo in federation_verify_certificates in config documentation. (#15139) (diff)
downloadsynapse-29cc17fde7f7438ccf826abe282bfbf8403c0d26.tar.xz
Add a method for Synapse modules to carry out HTTP federation requests
Provides a fairly basic interface for Synapse modules to complete HTTP
federation requests.

Custom error types were used in order to prevent stabilising any of the
internal MatrixFederationHttpClient code.
-rw-r--r--synapse/module_api/__init__.py132
-rw-r--r--synapse/module_api/errors.py56
2 files changed, 187 insertions, 1 deletions
diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py
index 1964276a54..af4dc3b1d5 100644
--- a/synapse/module_api/__init__.py
+++ b/synapse/module_api/__init__.py
@@ -37,7 +37,12 @@ from twisted.internet import defer
 from twisted.web.resource import Resource
 
 from synapse.api import errors
-from synapse.api.errors import SynapseError
+from synapse.api.errors import (
+    FederationDeniedError,
+    HttpResponseException,
+    RequestSendFailed,
+    SynapseError,
+)
 from synapse.events import EventBase
 from synapse.events.presence_router import (
     GET_INTERESTED_USERS_CALLBACK,
@@ -129,6 +134,14 @@ from synapse.util import Clock
 from synapse.util.async_helpers import maybe_awaitable
 from synapse.util.caches.descriptors import CachedFunction, cached as _cached
 from synapse.util.frozenutils import freeze
+from synapse.util.retryutils import NotRetryingDestination
+
+from .errors import (
+    FederationHttpDeniedException,
+    FederationHttpNotRetryingDestinationException,
+    FederationHttpRequestSendFailedException,
+    FederationHttpResponseException,
+)
 
 if TYPE_CHECKING:
     from synapse.app.generic_worker import GenericWorkerSlavedStore
@@ -1612,6 +1625,123 @@ class ModuleApi:
             deactivation=deactivation,
         )
 
+    async def _try_federation_http_request(
+        self,
+        method: str,
+        remote_server_name: str,
+        path: str,
+        query_parameters: Optional[Dict[str, Any]],
+        body: Optional[JsonDict] = None,
+        timeout: Optional[int] = None,
+    ) -> Union[JsonDict, List]:
+        """
+        Send a federation request to a remote homeserver and return the response.
+
+        This method assumes the `method` argument is fully capitalised.
+
+        A helper method for self.send_federation_http_request, see that method for
+        more details.
+        """
+        assert method in ["GET", "PUT", "POST", "DELETE"]
+
+        fed_client = self._hs.get_federation_http_client()
+
+        if method == "GET":
+            return await fed_client.get_json(
+                destination=remote_server_name,
+                path=path,
+                args=query_parameters,
+                timeout=timeout,
+            )
+        elif method == "PUT":
+            return await fed_client.put_json(
+                destination=remote_server_name,
+                path=path,
+                args=query_parameters,
+                data=body,
+                timeout=timeout,
+            )
+        elif method == "POST":
+            return await fed_client.post_json(
+                destination=remote_server_name,
+                path=path,
+                args=query_parameters,
+                data=body,
+                timeout=timeout,
+            )
+        elif method == "DELETE":
+            return await fed_client.delete_json(
+                destination=remote_server_name,
+                path=path,
+                args=query_parameters,
+                timeout=timeout,
+            )
+
+        return {}
+
+    async def send_federation_http_request(
+        self,
+        method: str,
+        remote_server_name: str,
+        path: str,
+        query_parameters: Optional[Dict[str, Any]],
+        body: Optional[JsonDict] = None,
+        timeout: Optional[int] = None,
+    ) -> Union[JsonDict, List]:
+        """
+        Send an HTTP federation request to a remote homeserver.
+
+        Added in Synapse v1.79.0.
+
+        If the request is successful, the parsed response body will be returned. If
+        unsuccessful, an exception will be raised. Callers are expected to handle the
+        possible exception cases. See exception class docstrings for a more detailed
+        explanation of each.
+
+        Args:
+            method: The HTTP method to use. Must be one of: "GET", "PUT", "POST",
+                "DELETE".
+            remote_server_name: The remote server to send the request to. This method
+                 will resolve delegated homeserver URLs automatically (well-known etc).
+            path: The HTTP path for the request.
+            query_parameters: Any query parameters for the request.
+            body: The body of the request.
+            timeout: The timeout in seconds to wait before giving up on a request.
+
+        Returns:
+            The response to the request as a Python object.
+
+        Raises:
+            FederationHttpResponseException: If we get an HTTP response code >= 300
+                (except 429).
+            FederationHttpNotRetryingDestinationException: If the homeserver believes the
+                remote homeserver is down and is not yet ready to attempt to contact it.
+            FederationHttpDeniedException: If this destination is not on the local
+                homeserver's configured federation whitelist.
+            FederationHttpRequestSendFailedException: If there were problems connecting
+                to the remote, due to e.g. DNS failures, connection timeouts etc.
+        """
+        try:
+            return await self._try_federation_http_request(
+                method.upper(), remote_server_name, path, query_parameters, body, timeout
+            )
+        except HttpResponseException as e:
+            raise FederationHttpResponseException(
+                remote_server_name,
+                status_code=e.code,
+                msg=e.msg,
+                response_body=e.response,
+            )
+        except NotRetryingDestination:
+            raise FederationHttpNotRetryingDestinationException(remote_server_name)
+        except FederationDeniedError:
+            raise FederationHttpDeniedException(remote_server_name)
+        except RequestSendFailed as e:
+            raise FederationHttpRequestSendFailedException(
+                remote_server_name,
+                can_retry=e.can_retry,
+            )
+
 
 class PublicRoomListManager:
     """Contains methods for adding to, removing from and querying whether a room
diff --git a/synapse/module_api/errors.py b/synapse/module_api/errors.py
index bedd045d6f..984fc8b3e5 100644
--- a/synapse/module_api/errors.py
+++ b/synapse/module_api/errors.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 """Exception types which are exposed as part of the stable module API"""
+import attr
 
 from synapse.api.errors import (
     Codes,
@@ -24,6 +25,57 @@ from synapse.config._base import ConfigError
 from synapse.handlers.push_rules import InvalidRuleException
 from synapse.storage.push_rule import RuleNotFoundException
 
+
+@attr.s(auto_attribs=True)
+class FederationHttpResponseException(Exception):
+    """
+    Raised when an HTTP request over federation returns a status code > 300 (and not 429).
+    """
+
+    remote_server_name: str
+    # The HTTP status code of the response.
+    status_code: int
+    # A human-readable explanation for the error.
+    msg: str
+    # The non-parsed HTTP response body.
+    response_body: bytes
+
+
+@attr.s(auto_attribs=True)
+class FederationHttpNotRetryingDestinationException(Exception):
+    """
+    Raised when the local homeserver refuses to send traffic to a remote homeserver that
+    it believes is experiencing an outage.
+    """
+
+    remote_server_name: str
+
+
+@attr.s(auto_attribs=True)
+class FederationHttpDeniedException(Exception):
+    """
+    Raised when the local homeserver refuses to send federation traffic to a remote
+    homeserver. This is due to the remote homeserver not being on the configured
+    federation whitelist.
+    """
+
+    remote_server_name: str
+
+
+@attr.s(auto_attribs=True)
+class FederationHttpRequestSendFailedException(Exception):
+    """
+    Raised when there are problems connecting to the remote homeserver due to e.g.
+    DNS failures, connection timeouts, etc.
+    """
+
+    remote_server_name: str
+    # Whether the request can be retried with a chance of success. This will be True
+    # if the failure occurred due to e.g. timeouts, a disruption in the connection etc.
+    # Will be false in the case of e.g. a malformed response from the remote homeserver.
+    can_retry: bool
+
+
 __all__ = [
     "Codes",
     "InvalidClientCredentialsError",
@@ -32,4 +84,8 @@ __all__ = [
     "ConfigError",
     "InvalidRuleException",
     "RuleNotFoundException",
+    "FederationHttpResponseException",
+    "FederationHttpNotRetryingDestinationException",
+    "FederationHttpDeniedException",
+    "FederationHttpRequestSendFailedException",
 ]