Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
SHHS - Room Join Complexity (#5072)
Browse files Browse the repository at this point in the history
  • Loading branch information
hawkowl authored May 20, 2019
1 parent d142e51 commit c99c105
Show file tree
Hide file tree
Showing 15 changed files with 462 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ _trial_temp*/
/*.signing.key
/env/
/homeserver*.yaml
/logs
/media_store/
/uploads

Expand All @@ -37,4 +38,3 @@ _trial_temp*/
/docs/build/
/htmlcov
/pip-wheel-metadata/

1 change: 1 addition & 0 deletions changelog.d/5072.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Synapse can now be configured to not join remote rooms of a given "complexity" (currently, state events). This option can be used to prevent adverse performance on resource-constrained homeservers.
11 changes: 11 additions & 0 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,17 @@ listeners:
# Used by phonehome stats to group together related servers.
#server_context: context

# Resource-constrained Homeserver Settings
#
# If limit_large_remote_room_joins is True, the room complexity will be
# checked before a user joins a new remote room. If it is above
# limit_large_remote_room_complexity, it will disallow joining or
# instantly leave.
#
# Uncomment the below lines to enable:
#limit_large_remote_room_joins: True
#limit_large_remote_room_complexity: 1.0

# Whether to require a user to be in the room to add an alias to it.
# Defaults to 'true'.
#
Expand Down
1 change: 1 addition & 0 deletions synapse/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
FEDERATION_PREFIX = "/_matrix/federation"
FEDERATION_V1_PREFIX = FEDERATION_PREFIX + "/v1"
FEDERATION_V2_PREFIX = FEDERATION_PREFIX + "/v2"
FEDERATION_UNSTABLE_PREFIX = FEDERATION_PREFIX + "/unstable"
STATIC_PREFIX = "/_matrix/static"
WEB_CLIENT_PREFIX = "/_matrix/client"
CONTENT_REPO_PREFIX = "/_matrix/content"
Expand Down
17 changes: 17 additions & 0 deletions synapse/config/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,12 @@ def read_config(self, config):

self.gc_thresholds = read_gc_thresholds(config.get("gc_thresholds", None))

# Resource-constrained Homeserver Configuration
self.limit_large_room_joins = config.get("limit_large_remote_room_joins", False)
self.limit_large_room_complexity = config.get(
"limit_large_remote_room_complexity", 1.0
)

bind_port = config.get("bind_port")
if bind_port:
if config.get("no_tls", False):
Expand Down Expand Up @@ -572,6 +578,17 @@ def default_config(self, server_name, data_dir_path, **kwargs):
# Used by phonehome stats to group together related servers.
#server_context: context
# Resource-constrained Homeserver Settings
#
# If limit_large_remote_room_joins is True, the room complexity will be
# checked before a user joins a new remote room. If it is above
# limit_large_remote_room_complexity, it will disallow joining or
# instantly leave.
#
# Uncomment the below lines to enable:
#limit_large_remote_room_joins: True
#limit_large_remote_room_complexity: 1.0
# Whether to require a user to be in the room to add an alias to it.
# Defaults to 'true'.
#
Expand Down
36 changes: 36 additions & 0 deletions synapse/federation/federation_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -992,3 +992,39 @@ def forward_third_party_invite(self, destinations, room_id, event_dict):
)

raise RuntimeError("Failed to send to any server.")

@defer.inlineCallbacks
def get_room_complexity(self, destination, room_id):
"""
Fetch the complexity of a remote room from another server.
Args:
destination (str): The remote server
room_id (str): The room ID to ask about.
Returns:
Deferred[dict] or Deferred[None]: Dict contains the complexity
metric versions, while None means we could not fetch the complexity.
"""
try:
complexity = yield self.transport_layer.get_room_complexity(
destination=destination,
room_id=room_id
)
defer.returnValue(complexity)
except CodeMessageException as e:
# We didn't manage to get it -- probably a 404. We are okay if other
# servers don't give it to us.
logger.debug(
"Failed to fetch room complexity via %s for %s, got a %d",
destination, room_id, e.code
)
except Exception:
logger.exception(
"Failed to fetch room complexity via %s for %s",
destination, room_id
)

# If we don't manage to find it, return None. It's not an error if a
# server doesn't give it to us.
defer.returnValue(None)
38 changes: 29 additions & 9 deletions synapse/federation/transport/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@
from twisted.internet import defer

from synapse.api.constants import Membership
from synapse.api.urls import FEDERATION_V1_PREFIX, FEDERATION_V2_PREFIX
from synapse.api.urls import (
FEDERATION_UNSTABLE_PREFIX,
FEDERATION_V1_PREFIX,
FEDERATION_V2_PREFIX,
)
from synapse.util.logutils import log_function

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -959,6 +963,28 @@ def bulk_get_publicised_groups(self, destination, user_ids):
ignore_backoff=True,
)

def get_room_complexity(self, destination, room_id):
"""
Args:
destination (str): The remote server
room_id (str): The room ID to ask about.
"""
path = _create_path(
FEDERATION_UNSTABLE_PREFIX, "/rooms/%s/complexity", room_id
)

return self.client.get_json(
destination=destination,
path=path
)


def _create_path(federation_prefix, path, *args):
"""
Ensures that all args are url encoded.
"""
return federation_prefix + path % tuple(urllib.parse.quote(arg, "") for arg in args)


def _create_v1_path(path, *args):
"""Creates a path against V1 federation API from the path template and
Expand All @@ -975,10 +1001,7 @@ def _create_v1_path(path, *args):
Returns:
str
"""
return (
FEDERATION_V1_PREFIX
+ path % tuple(urllib.parse.quote(arg, "") for arg in args)
)
return _create_path(FEDERATION_V1_PREFIX, path, *args)


def _create_v2_path(path, *args):
Expand All @@ -996,7 +1019,4 @@ def _create_v2_path(path, *args):
Returns:
str
"""
return (
FEDERATION_V2_PREFIX
+ path % tuple(urllib.parse.quote(arg, "") for arg in args)
)
return _create_path(FEDERATION_V2_PREFIX, path, *args)
27 changes: 26 additions & 1 deletion synapse/federation/transport/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
import synapse
from synapse.api.errors import Codes, FederationDeniedError, SynapseError
from synapse.api.room_versions import RoomVersions
from synapse.api.urls import FEDERATION_V1_PREFIX, FEDERATION_V2_PREFIX
from synapse.api.urls import (
FEDERATION_UNSTABLE_PREFIX,
FEDERATION_V1_PREFIX,
FEDERATION_V2_PREFIX,
)
from synapse.http.endpoint import parse_and_validate_server_name
from synapse.http.server import JsonResource
from synapse.http.servlet import (
Expand Down Expand Up @@ -1304,6 +1308,26 @@ def on_PUT(self, origin, content, query, group_id):
defer.returnValue((200, new_content))


class RoomComplexityServlet(BaseFederationServlet):
PATH = "/rooms/(?P<room_id>[^/]*)/complexity"
PREFIX = FEDERATION_UNSTABLE_PREFIX

@defer.inlineCallbacks
def on_GET(self, origin, content, query, room_id):

store = self.handler.hs.get_datastore()

is_public = yield store.is_room_world_readable_or_publicly_joinable(
room_id
)

if not is_public:
raise SynapseError(404, "Room not found", errcode=Codes.INVALID_PARAM)

complexity = yield store.get_room_complexity(room_id)
defer.returnValue((200, complexity))


FEDERATION_SERVLET_CLASSES = (
FederationSendServlet,
FederationEventServlet,
Expand All @@ -1327,6 +1351,7 @@ def on_PUT(self, origin, content, query, group_id):
FederationThirdPartyInviteExchangeServlet,
On3pidBindServlet,
FederationVersionServlet,
RoomComplexityServlet,
)

OPENID_SERVLET_CLASSES = (
Expand Down
25 changes: 25 additions & 0 deletions synapse/handlers/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2724,3 +2724,28 @@ def user_joined_room(self, user, room_id):
)
else:
return user_joined_room(self.distributor, user, room_id)

@defer.inlineCallbacks
def get_room_complexity(self, remote_room_hosts, room_id):
"""
Fetch the complexity of a remote room over federation.
Args:
remote_room_hosts (list[str]): The remote servers to ask.
room_id (str): The room ID to ask about.
Returns:
Deferred[dict] or Deferred[None]: Dict contains the complexity
metric versions, while None means we could not fetch the complexity.
"""

for host in remote_room_hosts:
res = yield self.federation_client.get_room_complexity(host, room_id)

# We got a result, return it.
if res:
defer.returnValue(res)

# We fell off the bottom, couldn't get the complexity from anyone. Oh
# well.
defer.returnValue(None)
89 changes: 85 additions & 4 deletions synapse/handlers/room_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@

from twisted.internet import defer

import synapse.server
import synapse.types
from synapse import types
from synapse.api.constants import EventTypes, Membership
from synapse.api.errors import AuthError, Codes, SynapseError
from synapse.types import RoomID, UserID
Expand Down Expand Up @@ -590,7 +589,7 @@ def send_membership_event(
)
assert self.hs.is_mine(sender), "Sender must be our own: %s" % (sender,)
else:
requester = synapse.types.create_requester(target_user)
requester = types.create_requester(target_user)

prev_event = yield self.event_creation_handler.deduplicate_state_event(
event, context,
Expand Down Expand Up @@ -1017,21 +1016,73 @@ def __init__(self, hs):
self.distributor.declare("user_joined_room")
self.distributor.declare("user_left_room")

@defer.inlineCallbacks
def _is_remote_room_too_complex(self, room_id, remote_room_hosts):
"""
Check if complexity of a remote room is too great.
Args:
room_id (str)
remote_room_hosts (list[str])
Returns: bool of whether the complexity is too great, or None
if unable to be fetched
"""
max_complexity = self.hs.config.limit_large_room_complexity
complexity = yield self.federation_handler.get_room_complexity(
remote_room_hosts, room_id
)

if complexity:
if complexity["v1"] > max_complexity:
return True
return False
return None

@defer.inlineCallbacks
def _is_local_room_too_complex(self, room_id):
"""
Check if the complexity of a local room is too great.
Args:
room_id (str)
Returns: bool
"""
max_complexity = self.hs.config.limit_large_room_complexity
complexity = yield self.store.get_room_complexity(room_id)

if complexity["v1"] > max_complexity:
return True

return False

@defer.inlineCallbacks
def _remote_join(self, requester, remote_room_hosts, room_id, user, content):
"""Implements RoomMemberHandler._remote_join
"""
# filter ourselves out of remote_room_hosts: do_invite_join ignores it
# and if it is the only entry we'd like to return a 404 rather than a
# 500.

remote_room_hosts = [
host for host in remote_room_hosts if host != self.hs.hostname
]

if len(remote_room_hosts) == 0:
raise SynapseError(404, "No known servers")

if self.hs.config.limit_large_room_joins:
# Fetch the room complexity
too_complex = yield self._is_remote_room_too_complex(
room_id, remote_room_hosts
)
if too_complex is True:
msg = "Room too large (preflight)"
raise SynapseError(
code=400, msg=msg,
errcode=Codes.RESOURCE_LIMIT_EXCEEDED
)

# We don't do an auth check if we are doing an invite
# join dance for now, since we're kinda implicitly checking
# that we are allowed to join when we decide whether or not we
Expand All @@ -1044,6 +1095,36 @@ def _remote_join(self, requester, remote_room_hosts, room_id, user, content):
)
yield self._user_joined_room(user, room_id)

# Check the room we just joined wasn't too large, if we didn't fetch the
# complexity of it before.
if self.hs.config.limit_large_room_joins:
if too_complex is False:
# We checked, and we're under the limit.
return

# Check again, but with the local state events
too_complex = yield self._is_local_room_too_complex(room_id)

if too_complex is False:
# We're under the limit.
return

# The room is too large. Leave.
requester = types.create_requester(
user, None, False, None
)
yield self.update_membership(
requester=requester,
target=user,
room_id=room_id,
action="leave"
)
msg = "Room too large (postflight)"
raise SynapseError(
code=400, msg=msg,
errcode=Codes.RESOURCE_LIMIT_EXCEEDED
)

@defer.inlineCallbacks
def _remote_reject_invite(self, requester, remote_room_hosts, room_id, target):
"""Implements RoomMemberHandler._remote_reject_invite
Expand Down
Loading

0 comments on commit c99c105

Please sign in to comment.