mirror of
https://github.com/element-hq/synapse
synced 2024-09-05 22:20:06 +00:00
draft for federation of knock (untested)
This commit is contained in:
parent
057740f283
commit
ddd3584bdc
6 changed files with 302 additions and 3 deletions
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015, 2016 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
# Copyrignt 2020 Sorunome
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -438,7 +439,7 @@ class FederationClient(FederationBase):
|
||||||
|
|
||||||
Fails with a ``RuntimeError`` if no servers were reachable.
|
Fails with a ``RuntimeError`` if no servers were reachable.
|
||||||
"""
|
"""
|
||||||
valid_memberships = {Membership.JOIN, Membership.LEAVE}
|
valid_memberships = {Membership.JOIN, Membership.LEAVE, Membership.KNOCK}
|
||||||
if membership not in valid_memberships:
|
if membership not in valid_memberships:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"make_membership_event called with membership='%s', must be one of %s"
|
"make_membership_event called with membership='%s', must be one of %s"
|
||||||
|
@ -785,6 +786,48 @@ class FederationClient(FederationBase):
|
||||||
# content.
|
# content.
|
||||||
return resp[1]
|
return resp[1]
|
||||||
|
|
||||||
|
def send_knock(self, destinations, pdu):
|
||||||
|
"""Sends a knock event to one of a list of homeservers.
|
||||||
|
|
||||||
|
Doing so will cause the remote server to add teh event to the graph,
|
||||||
|
and send the event out to the rest of the federation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
destinations (str): Candidate homeservers which are probably participating in the room.
|
||||||
|
pdu (BaseEvent): event to be sent
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Deferred: resolves to None.
|
||||||
|
|
||||||
|
Fails with a ``SynapseError`` if the chosen remote server
|
||||||
|
returns a 300/400 code.
|
||||||
|
|
||||||
|
Fails with a ``RuntimeError`` if no servers were reachable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def send_request(destination):
|
||||||
|
content = yield self._do_send_knock(destination, pdu)
|
||||||
|
|
||||||
|
logger.debug("Got content: %s", content)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._try_destination_list("send_knock", destinations, send_request)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _do_send_knock(self, destination, pdu):
|
||||||
|
time_now = self._clock.time_msec()
|
||||||
|
|
||||||
|
# knock only has the v2 api, no need to fall back to v1
|
||||||
|
content = yield self.transport_layer.send_knock_v2(
|
||||||
|
destination=destination,
|
||||||
|
room_id=pdu.room_id,
|
||||||
|
event_id=pdu.event_id,
|
||||||
|
content=pdu.get_pdu_json(time_now)
|
||||||
|
)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
def get_public_rooms(
|
def get_public_rooms(
|
||||||
self,
|
self,
|
||||||
destination,
|
destination,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014-2016 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
# Copyright 2018 New Vector Ltd
|
# Copyright 2018 New Vector Ltd
|
||||||
|
# Copyright 2020 Sorunome
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -299,6 +300,17 @@ class TransportLayerClient(object):
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def send_knock_v2(self, destination, room_id, event_id, content):
|
||||||
|
path = _create_v2_path("/send_knock/%s/%s", room_id, event_id)
|
||||||
|
|
||||||
|
response = yield self.client.put_json(
|
||||||
|
destination=destination, path=path, data=content
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def send_invite_v1(self, destination, room_id, event_id, content):
|
def send_invite_v1(self, destination, room_id, event_id, content):
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# Copyright 2014-2016 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
# Copyright 2018 New Vector Ltd
|
# Copyright 2018 New Vector Ltd
|
||||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
# Copyright 2020 Sorunome
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -541,6 +542,22 @@ class FederationV2SendLeaveServlet(BaseFederationServlet):
|
||||||
return 200, content
|
return 200, content
|
||||||
|
|
||||||
|
|
||||||
|
class FederationMakeKnockServlet(BaseFederationServlet):
|
||||||
|
PATH = "/make_knock/(?P<context>[^/]*)/(?P<user_id>[^/]*)"
|
||||||
|
|
||||||
|
async def on_GET(self, origin, content, query, context, user_id):
|
||||||
|
content = await self.handler.on_make_knock_request(origin, context, user_id)
|
||||||
|
return 200, content
|
||||||
|
|
||||||
|
class FederationV2MakeKnockServlet(BaseFederationServlet):
|
||||||
|
PATH = "/send_knock/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)"
|
||||||
|
|
||||||
|
PREFIX = FEDERATION_V2_PREFIX
|
||||||
|
|
||||||
|
async def on_PUT(self, origin, content, query, room_id, event_id):
|
||||||
|
content = await self.handler.on_send_knock_request(origin, content, room_id)
|
||||||
|
return 200, content
|
||||||
|
|
||||||
class FederationEventAuthServlet(BaseFederationServlet):
|
class FederationEventAuthServlet(BaseFederationServlet):
|
||||||
PATH = "/event_auth/(?P<context>[^/]*)/(?P<event_id>[^/]*)"
|
PATH = "/event_auth/(?P<context>[^/]*)/(?P<event_id>[^/]*)"
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# Copyright 2014-2016 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
# Copyright 2017-2018 New Vector Ltd
|
# Copyright 2017-2018 New Vector Ltd
|
||||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
# Copyright 2020 Sorunome
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -1273,6 +1274,34 @@ class FederationHandler(BaseHandler):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@log_function
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def do_knock(self, target_hosts, room_id, knockee, content):
|
||||||
|
""" Sends the knock to the remote server.
|
||||||
|
|
||||||
|
This first triggers a /make_knock/ request that returns a partial
|
||||||
|
event that we can fill out and sign. This is then sent to the
|
||||||
|
remote server via /send_knock/.
|
||||||
|
|
||||||
|
Knockees must be signed by the knockee's server before distributing.
|
||||||
|
"""
|
||||||
|
logger.debug("Knocking %s to %s", knockee, room_id)
|
||||||
|
|
||||||
|
origin, event, event_format_version = yield self._make_and_verify_event(
|
||||||
|
target_hosts, room_id, knockee, "knock", content,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try the host that we successfully called /make_knock/ on first for
|
||||||
|
# the /send_knock/ request.
|
||||||
|
try:
|
||||||
|
target_hosts.remove(origin)
|
||||||
|
target_hosts.insert(0, origin)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
yield self.federation_client.send_knock(target_hosts, event)
|
||||||
|
return event
|
||||||
|
|
||||||
async def _handle_queued_pdus(self, room_queue):
|
async def _handle_queued_pdus(self, room_queue):
|
||||||
"""Process PDUs which got queued up while we were busy send_joining.
|
"""Process PDUs which got queued up while we were busy send_joining.
|
||||||
|
|
||||||
|
@ -1628,6 +1657,107 @@ class FederationHandler(BaseHandler):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def on_make_kock_request(self, origin, room_id, user_id):
|
||||||
|
""" We've received a /make_knock/ request, so we create a partial
|
||||||
|
knock event for the room and return that. We do *not* persist or
|
||||||
|
process it until the other server has signed it and sent it back.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
origin (str): The (verified) server name of the requesting server.
|
||||||
|
room_id (str): Room to create knock event in
|
||||||
|
user_id (str): The user to create the knock for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Deferred[FrozenEvent]
|
||||||
|
"""
|
||||||
|
if get_domain_from_id(user_id) != origin:
|
||||||
|
logger.info(
|
||||||
|
"Get /make_knock request for user %r from different origin %s, ignoring",
|
||||||
|
user_id,
|
||||||
|
origin,
|
||||||
|
)
|
||||||
|
raise SynapseError(403, "User not from origin", Codes.FORBIDDEN)
|
||||||
|
|
||||||
|
room_version = yield self.store.get_room_version(room_id)
|
||||||
|
builder = self.event_builder_factory.new(
|
||||||
|
room_version,
|
||||||
|
{
|
||||||
|
"type": EventTypes.Member,
|
||||||
|
"content": {"membership": Membership.KNOCK},
|
||||||
|
"room_id": room_id,
|
||||||
|
"sender": user_id,
|
||||||
|
"state_key": user_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
event, context = yield self.event_creation_handler.create_new_client_event(
|
||||||
|
builder=builder
|
||||||
|
)
|
||||||
|
|
||||||
|
event_allowed = yield self.third_party_event_rules.check_event_allowed(
|
||||||
|
event, context
|
||||||
|
)
|
||||||
|
if not event_allowed:
|
||||||
|
logger.warning("Creation of leave %s forbidden by third-party rules", event)
|
||||||
|
raise SynapseError(
|
||||||
|
403, "This event is not allowed in this context", Codes.FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# The remote hasn't signed it yet, obviously. We'll do the full checks
|
||||||
|
# when we get the event back in `on_send_knock_request`
|
||||||
|
yield self.auth.check_from_context(
|
||||||
|
room_version, event, context, do_sig_check=False
|
||||||
|
)
|
||||||
|
except AuthError as e:
|
||||||
|
logger.warning("Failed to create new knock %r because %s", event, e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
return event
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def on_send_knock_request(self, origin, pdu):
|
||||||
|
""" We have received a knock event for a room. Fully process it."""
|
||||||
|
event = pdu
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"on_send_knock_request: Got event: %s, signatures: %s",
|
||||||
|
event.event_id,
|
||||||
|
event.signatures,
|
||||||
|
)
|
||||||
|
|
||||||
|
if get_domain_from_id(event.sender) != origin:
|
||||||
|
logger.info(
|
||||||
|
"Got /send_knock request for user %r from different origin %s",
|
||||||
|
event.sender,
|
||||||
|
origin,
|
||||||
|
)
|
||||||
|
raise SynapseError(403, "User not from origin", Codes.FORBIDDEN)
|
||||||
|
|
||||||
|
event.internal_metadata.outlier = False
|
||||||
|
|
||||||
|
context = yield self._handle_new_event(origin, event)
|
||||||
|
|
||||||
|
event_allowed = yield self.third_party_event_rules.check_event_allowed(
|
||||||
|
event, context
|
||||||
|
)
|
||||||
|
if not event_allowed:
|
||||||
|
logger.info("Sending of leave %s forbidden by third-party rules", event)
|
||||||
|
raise SynapseError(
|
||||||
|
403, "This event is not allowed in this context", Codes.FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"on_send_knock_request: After _handle_new_event: %s, sigs: %s",
|
||||||
|
event.event_id,
|
||||||
|
event.signatures,
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_state_for_pdu(self, room_id, event_id):
|
def get_state_for_pdu(self, room_id, event_id):
|
||||||
"""Returns the state at the event. i.e. not including said event.
|
"""Returns the state at the event. i.e. not including said event.
|
||||||
|
|
|
@ -489,7 +489,11 @@ class RoomMemberHandler(object):
|
||||||
content["displayname"] = yield profile.get_displayname(target)
|
content["displayname"] = yield profile.get_displayname(target)
|
||||||
content["avatar_url"] = yield profile.get_avatar_url(target)
|
content["avatar_url"] = yield profile.get_avatar_url(target)
|
||||||
|
|
||||||
raise SynapseError(500, "Not yet implemented")
|
remote_knock_response = yield self._remote_knock(
|
||||||
|
requester, remote_room_hosts, room_id, target, content
|
||||||
|
)
|
||||||
|
|
||||||
|
return remote_knock_response
|
||||||
|
|
||||||
res = yield self._local_membership_update(
|
res = yield self._local_membership_update(
|
||||||
requester=requester,
|
requester=requester,
|
||||||
|
@ -1012,6 +1016,25 @@ class RoomMemberMasterHandler(RoomMemberHandler):
|
||||||
yield self.store.locally_reject_invite(target.to_string(), room_id)
|
yield self.store.locally_reject_invite(target.to_string(), room_id)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _remote_knock(self, requester, remote_room_hosts, room_id, user, content):
|
||||||
|
# filter ourselves out of remote_room_hosts
|
||||||
|
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")
|
||||||
|
|
||||||
|
ret = yield fed_handler.do_knock(
|
||||||
|
remote_room_hosts, room_id, user.to_string(), content=content,
|
||||||
|
)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
yield self.federation_handler.send_knock(
|
||||||
|
remote_room_hosts, room_id, user.to_string(), content
|
||||||
|
)
|
||||||
|
|
||||||
def _user_joined_room(self, target, room_id):
|
def _user_joined_room(self, target, room_id):
|
||||||
"""Implements RoomMemberHandler._user_joined_room
|
"""Implements RoomMemberHandler._user_joined_room
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -17,13 +17,19 @@ import logging
|
||||||
|
|
||||||
from synapse.api.errors import AuthError, SynapseError
|
from synapse.api.errors import AuthError, SynapseError
|
||||||
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
||||||
|
from synapse.rest.client.transactions import HttpTransactionCache
|
||||||
|
from synapse.types import RoomAlias, RoomID, StreamToken, ThirdPartyInstanceID, UserID
|
||||||
|
|
||||||
from ._base import client_patterns
|
from ._base import client_patterns
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class TransactionRestServlet(RestServlet):
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(TransactionRestServlet, self).__init__()
|
||||||
|
self.txns = HttpTransactionCache(hs)
|
||||||
|
|
||||||
class KnockServlet(RestServlet):
|
class KnockServlet(TransactionRestServlet):
|
||||||
"""
|
"""
|
||||||
POST /rooms/{roomId}/knock
|
POST /rooms/{roomId}/knock
|
||||||
"""
|
"""
|
||||||
|
@ -57,5 +63,73 @@ class KnockServlet(RestServlet):
|
||||||
|
|
||||||
return 200, {}
|
return 200, {}
|
||||||
|
|
||||||
|
def on_PUT(self, request, room_id, txn_id):
|
||||||
|
set_tag("txn_id", txn_id)
|
||||||
|
|
||||||
|
return self.txns.fetch_or_execute_request(
|
||||||
|
request, self.on_POST, request, room_id, txn_id
|
||||||
|
)
|
||||||
|
|
||||||
|
class KnockRoomALiasServlet(TransactionRestServlet):
|
||||||
|
"""
|
||||||
|
POST /knock/{roomIdOrAlias}
|
||||||
|
"""
|
||||||
|
|
||||||
|
PATTERNS = client_patterns(
|
||||||
|
"/knock/(?P<room_identifier>[^/]*)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(KnockRoomALiasServlet, self).__init__()
|
||||||
|
self.room_member_handler = hs.get_room_member_handler()
|
||||||
|
self.auth = hs.get_auth()
|
||||||
|
|
||||||
|
async def on_POST(self, request, room_identifier, txn_id=None):
|
||||||
|
requester = await self.auth.get_user_by_req(request)
|
||||||
|
|
||||||
|
content = parse_json_object_from_request(request)
|
||||||
|
event_content = None
|
||||||
|
if "reason" in content:
|
||||||
|
event_content = {"reason": content["reason"]}
|
||||||
|
|
||||||
|
if RoomID.is_valid(room_identifier):
|
||||||
|
room_id = room_identifier
|
||||||
|
try:
|
||||||
|
remote_room_hosts = [
|
||||||
|
x.decode("ascii") for x in request.args[b"server_name"]
|
||||||
|
]
|
||||||
|
except Exception:
|
||||||
|
remote_room_hosts = None
|
||||||
|
elif RoomAlias.is_valid(room_identifier):
|
||||||
|
handler = self.room_member_handler
|
||||||
|
room_alias = RoomAlias.from_string(room_identifier)
|
||||||
|
room_id, remote_room_hosts = await handler.lookup_room_alias(room_alias)
|
||||||
|
room_id = room_id.to_string()
|
||||||
|
else:
|
||||||
|
raise SynapseError(
|
||||||
|
400, "%s was not legal room ID or room alias" % (room_identifier,)
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.room_member_handler.update_membership(
|
||||||
|
requester=requester,
|
||||||
|
target=requester.user,
|
||||||
|
room_id=room_id,
|
||||||
|
action="knock",
|
||||||
|
txn_id=txn_id,
|
||||||
|
third_party_signed=None,
|
||||||
|
remote_room_hosts=remote_room_hosts,
|
||||||
|
content=event_content,
|
||||||
|
)
|
||||||
|
|
||||||
|
return 200, {}
|
||||||
|
|
||||||
|
def on_PUT(self, request, room_identifier, txn_id):
|
||||||
|
set_tag("txn_id", txn_id)
|
||||||
|
|
||||||
|
return self.txns.fetch_or_execute_request(
|
||||||
|
request, self.on_POST, request, room_identifier, txn_id
|
||||||
|
)
|
||||||
|
|
||||||
def register_servlets(hs, http_server):
|
def register_servlets(hs, http_server):
|
||||||
KnockServlet(hs).register(http_server)
|
KnockServlet(hs).register(http_server)
|
||||||
|
KnockServlet(hs).register(http_server)
|
||||||
|
|
Loading…
Reference in a new issue