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

Support for room versioning #3654

Merged
merged 8 commits into from
Aug 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/3654.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Basic support for room versioning
9 changes: 9 additions & 0 deletions synapse/api/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2014-2016 OpenMarket Ltd
# Copyright 2017 Vector Creations Ltd
# Copyright 2018 New Vector Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -94,3 +95,11 @@ class RoomCreationPreset(object):
class ThirdPartyEntityKind(object):
USER = "user"
LOCATION = "location"


# the version we will give rooms which are created on this server
DEFAULT_ROOM_VERSION = "1"

# vdh-test-version is a placeholder to get room versioning support working and tested
# until we have a working v2.
KNOWN_ROOM_VERSIONS = {"1", "vdh-test-version"}
24 changes: 24 additions & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2014-2016 OpenMarket Ltd
# Copyright 2018 New Vector Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -56,6 +57,8 @@ class Codes(object):
CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM"
MAU_LIMIT_EXCEEDED = "M_MAU_LIMIT_EXCEEDED"
UNSUPPORTED_ROOM_VERSION = "M_UNSUPPORTED_ROOM_VERSION"
INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"


class CodeMessageException(RuntimeError):
Expand Down Expand Up @@ -285,6 +288,27 @@ def error_dict(self):
)


class IncompatibleRoomVersionError(SynapseError):
"""A server is trying to join a room whose version it does not support."""

def __init__(self, room_version):
super(IncompatibleRoomVersionError, self).__init__(
code=400,
msg="Your homeserver does not support the features required to "
"join this room",
errcode=Codes.INCOMPATIBLE_ROOM_VERSION,
)

self._room_version = room_version

def error_dict(self):
return cs_error(
self.msg,
self.errcode,
room_version=self._room_version,
)


def cs_error(msg, code=Codes.UNKNOWN, **kwargs):
""" Utility method for constructing an error response for client-server
interactions.
Expand Down
10 changes: 9 additions & 1 deletion synapse/event_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from signedjson.sign import SignatureVerifyException, verify_signed_json
from unpaddedbase64 import decode_base64

from synapse.api.constants import EventTypes, JoinRules, Membership
from synapse.api.constants import KNOWN_ROOM_VERSIONS, EventTypes, JoinRules, Membership
from synapse.api.errors import AuthError, EventSizeError, SynapseError
from synapse.types import UserID, get_domain_from_id

Expand Down Expand Up @@ -83,6 +83,14 @@ def check(event, auth_events, do_sig_check=True, do_size_check=True):
403,
"Creation event's room_id domain does not match sender's"
)

room_version = event.content.get("room_version", "1")
if room_version not in KNOWN_ROOM_VERSIONS:
raise AuthError(
403,
"room appears to have unsupported version %s" % (
room_version,
))
# FIXME
logger.debug("Allowing! %s", event)
return
Expand Down
38 changes: 31 additions & 7 deletions synapse/federation/federation_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

from twisted.internet import defer

from synapse.api.constants import Membership
from synapse.api.constants import KNOWN_ROOM_VERSIONS, EventTypes, Membership
from synapse.api.errors import (
CodeMessageException,
FederationDeniedError,
Expand Down Expand Up @@ -518,10 +518,10 @@ def _try_destination_list(self, description, destinations, callback):
description, destination, exc_info=1,
)

raise RuntimeError("Failed to %s via any server", description)
raise RuntimeError("Failed to %s via any server" % (description, ))

def make_membership_event(self, destinations, room_id, user_id, membership,
content={},):
content, params):
"""
Creates an m.room.member event, with context, without participating in the room.

Expand All @@ -537,8 +537,10 @@ def make_membership_event(self, destinations, room_id, user_id, membership,
user_id (str): The user whose membership is being evented.
membership (str): The "membership" property of the event. Must be
one of "join" or "leave".
content (object): Any additional data to put into the content field
content (dict): Any additional data to put into the content field
of the event.
params (dict[str, str|Iterable[str]]): Query parameters to include in the
request.
Return:
Deferred: resolves to a tuple of (origin (str), event (object))
where origin is the remote homeserver which generated the event.
Expand All @@ -558,10 +560,12 @@ def make_membership_event(self, destinations, room_id, user_id, membership,
@defer.inlineCallbacks
def send_request(destination):
ret = yield self.transport_layer.make_membership_event(
destination, room_id, user_id, membership
destination, room_id, user_id, membership, params,
)

pdu_dict = ret["event"]
pdu_dict = ret.get("event", None)
if not isinstance(pdu_dict, dict):
raise InvalidResponseError("Bad 'event' field in response")

logger.debug("Got response to make_%s: %s", membership, pdu_dict)

Expand Down Expand Up @@ -605,6 +609,26 @@ def send_join(self, destinations, pdu):
Fails with a ``RuntimeError`` if no servers were reachable.
"""

def check_authchain_validity(signed_auth_chain):
for e in signed_auth_chain:
if e.type == EventTypes.Create:
create_event = e
break
else:
raise InvalidResponseError(
"no %s in auth chain" % (EventTypes.Create,),
)

# the room version should be sane.
room_version = create_event.content.get("room_version", "1")
if room_version not in KNOWN_ROOM_VERSIONS:
# This shouldn't be possible, because the remote server should have
# rejected the join attempt during make_join.
raise InvalidResponseError(
"room appears to have unsupported version %s" % (
room_version,
))

@defer.inlineCallbacks
def send_request(destination):
time_now = self._clock.time_msec()
Expand Down Expand Up @@ -661,7 +685,7 @@ def send_request(destination):
for s in signed_state:
s.internal_metadata = copy.deepcopy(s.internal_metadata)

auth_chain.sort(key=lambda e: e.depth)
check_authchain_validity(signed_auth)

defer.returnValue({
"state": signed_state,
Expand Down
21 changes: 18 additions & 3 deletions synapse/federation/federation_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@
from twisted.python import failure

from synapse.api.constants import EventTypes
from synapse.api.errors import AuthError, FederationError, NotFoundError, SynapseError
from synapse.api.errors import (
AuthError,
FederationError,
IncompatibleRoomVersionError,
NotFoundError,
SynapseError,
)
from synapse.crypto.event_signing import compute_event_signature
from synapse.federation.federation_base import FederationBase, event_from_pdu_json
from synapse.federation.persistence import TransactionActions
Expand Down Expand Up @@ -323,12 +329,21 @@ def on_query_request(self, query_type, args):
defer.returnValue((200, resp))

@defer.inlineCallbacks
def on_make_join_request(self, origin, room_id, user_id):
def on_make_join_request(self, origin, room_id, user_id, supported_versions):
origin_host, _ = parse_server_name(origin)
yield self.check_server_matches_acl(origin_host, room_id)

room_version = yield self.store.get_room_version(room_id)
if room_version not in supported_versions:
logger.warn("Room version %s not in %s", room_version, supported_versions)
raise IncompatibleRoomVersionError(room_version=room_version)

pdu = yield self.handler.on_make_join_request(room_id, user_id)
time_now = self._clock.time_msec()
defer.returnValue({"event": pdu.get_pdu_json(time_now)})
defer.returnValue({
"event": pdu.get_pdu_json(time_now),
"room_version": room_version,
})

@defer.inlineCallbacks
def on_invite_request(self, origin, content):
Expand Down
5 changes: 4 additions & 1 deletion synapse/federation/transport/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def make_query(self, destination, query_type, args, retry_on_dns_fail,

@defer.inlineCallbacks
@log_function
def make_membership_event(self, destination, room_id, user_id, membership):
def make_membership_event(self, destination, room_id, user_id, membership, params):
"""Asks a remote server to build and sign us a membership event

Note that this does not append any events to any graphs.
Expand All @@ -205,6 +205,8 @@ def make_membership_event(self, destination, room_id, user_id, membership):
room_id (str): room to join/leave
user_id (str): user to be joined/left
membership (str): one of join/leave
params (dict[str, str|Iterable[str]]): Query parameters to include in the
request.

Returns:
Deferred: Succeeds when we get a 2xx HTTP response. The result
Expand Down Expand Up @@ -241,6 +243,7 @@ def make_membership_event(self, destination, room_id, user_id, membership):
content = yield self.client.get_json(
destination=destination,
path=path,
args=params,
retry_on_dns_fail=retry_on_dns_fail,
timeout=20000,
ignore_backoff=ignore_backoff,
Expand Down
71 changes: 70 additions & 1 deletion synapse/federation/transport/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,41 @@ def strip_quotes(value):


class BaseFederationServlet(object):
"""Abstract base class for federation servlet classes.

The servlet object should have a PATH attribute which takes the form of a regexp to
match against the request path (excluding the /federation/v1 prefix).

The servlet should also implement one or more of on_GET, on_POST, on_PUT, to match
the appropriate HTTP method. These methods have the signature:

on_<METHOD>(self, origin, content, query, **kwargs)

With arguments:

origin (unicode|None): The authenticated server_name of the calling server,
unless REQUIRE_AUTH is set to False and authentication failed.

content (unicode|None): decoded json body of the request. None if the
request was a GET.

query (dict[bytes, list[bytes]]): Query params from the request. url-decoded
(ie, '+' and '%xx' are decoded) but note that it is *not* utf8-decoded
yet.

**kwargs (dict[unicode, unicode]): the dict mapping keys to path
components as specified in the path match regexp.

Returns:
Deferred[(int, object)|None]: either (response code, response object) to
return a JSON response, or None if the request has already been handled.

Raises:
SynapseError: to return an error code

Exception: other exceptions will be caught, logged, and a 500 will be
returned.
"""
REQUIRE_AUTH = True

def __init__(self, handler, authenticator, ratelimiter, server_name):
Expand All @@ -204,6 +239,18 @@ def _wrap(self, func):
@defer.inlineCallbacks
@functools.wraps(func)
def new_func(request, *args, **kwargs):
""" A callback which can be passed to HttpServer.RegisterPaths

Args:
request (twisted.web.http.Request):
*args: unused?
**kwargs (dict[unicode, unicode]): the dict mapping keys to path
components as specified in the path match regexp.

Returns:
Deferred[(int, object)|None]: (response code, response object) as returned
by the callback method. None if the request has already been handled.
"""
content = None
if request.method in ["PUT", "POST"]:
# TODO: Handle other method types? other content types?
Expand Down Expand Up @@ -384,9 +431,31 @@ class FederationMakeJoinServlet(BaseFederationServlet):
PATH = "/make_join/(?P<context>[^/]*)/(?P<user_id>[^/]*)"

@defer.inlineCallbacks
def on_GET(self, origin, content, query, context, user_id):
def on_GET(self, origin, _content, query, context, user_id):
"""
Args:
origin (unicode): The authenticated server_name of the calling server

_content (None): (GETs don't have bodies)

query (dict[bytes, list[bytes]]): Query params from the request.

**kwargs (dict[unicode, unicode]): the dict mapping keys to path
components as specified in the path match regexp.

Returns:
Deferred[(int, object)|None]: either (response code, response object) to
return a JSON response, or None if the request has already been handled.
"""
versions = query.get(b'ver')
if versions is not None:
supported_versions = [v.decode("utf-8") for v in versions]
else:
supported_versions = ["1"]

content = yield self.handler.on_make_join_request(
origin, context, user_id,
supported_versions=supported_versions,
)
defer.returnValue((200, content))

Expand Down
13 changes: 11 additions & 2 deletions synapse/handlers/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@

from twisted.internet import defer

from synapse.api.constants import EventTypes, Membership, RejectedReason
from synapse.api.constants import (
KNOWN_ROOM_VERSIONS,
EventTypes,
Membership,
RejectedReason,
)
from synapse.api.errors import (
AuthError,
CodeMessageException,
Expand Down Expand Up @@ -922,6 +927,9 @@ def do_invite_join(self, target_hosts, room_id, joinee, content):
joinee,
"join",
content,
params={
"ver": KNOWN_ROOM_VERSIONS,
},
)

# This shouldn't happen, because the RoomMemberHandler has a
Expand Down Expand Up @@ -1187,13 +1195,14 @@ def do_remotely_reject_invite(self, target_hosts, room_id, user_id):

@defer.inlineCallbacks
def _make_and_verify_event(self, target_hosts, room_id, user_id, membership,
content={},):
content={}, params=None):
origin, pdu = yield self.federation_client.make_membership_event(
target_hosts,
room_id,
user_id,
membership,
content,
params=params,
)

logger.debug("Got response to make_%s: %s", membership, pdu)
Expand Down
Loading