mirror of
https://github.com/element-hq/synapse
synced 2024-10-06 13:32:40 +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 -*-
|
||||
# Copyright 2015, 2016 OpenMarket Ltd
|
||||
# Copyrignt 2020 Sorunome
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (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.
|
||||
"""
|
||||
valid_memberships = {Membership.JOIN, Membership.LEAVE}
|
||||
valid_memberships = {Membership.JOIN, Membership.LEAVE, Membership.KNOCK}
|
||||
if membership not in valid_memberships:
|
||||
raise RuntimeError(
|
||||
"make_membership_event called with membership='%s', must be one of %s"
|
||||
|
@ -785,6 +786,48 @@ class FederationClient(FederationBase):
|
|||
# content.
|
||||
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(
|
||||
self,
|
||||
destination,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2018 New Vector Ltd
|
||||
# Copyright 2020 Sorunome
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -299,6 +300,17 @@ class TransportLayerClient(object):
|
|||
|
||||
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
|
||||
@log_function
|
||||
def send_invite_v1(self, destination, room_id, event_id, content):
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2018 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
# Copyright 2020 Sorunome
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -541,6 +542,22 @@ class FederationV2SendLeaveServlet(BaseFederationServlet):
|
|||
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):
|
||||
PATH = "/event_auth/(?P<context>[^/]*)/(?P<event_id>[^/]*)"
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2017-2018 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
# Copyright 2020 Sorunome
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -1273,6 +1274,34 @@ class FederationHandler(BaseHandler):
|
|||
|
||||
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):
|
||||
"""Process PDUs which got queued up while we were busy send_joining.
|
||||
|
||||
|
@ -1628,6 +1657,107 @@ class FederationHandler(BaseHandler):
|
|||
|
||||
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
|
||||
def get_state_for_pdu(self, room_id, event_id):
|
||||
"""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["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(
|
||||
requester=requester,
|
||||
|
@ -1012,6 +1016,25 @@ class RoomMemberMasterHandler(RoomMemberHandler):
|
|||
yield self.store.locally_reject_invite(target.to_string(), room_id)
|
||||
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):
|
||||
"""Implements RoomMemberHandler._user_joined_room
|
||||
"""
|
||||
|
|
|
@ -17,13 +17,19 @@ import logging
|
|||
|
||||
from synapse.api.errors import AuthError, SynapseError
|
||||
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
|
||||
|
||||
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
|
||||
"""
|
||||
|
@ -57,5 +63,73 @@ class KnockServlet(RestServlet):
|
|||
|
||||
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):
|
||||
KnockServlet(hs).register(http_server)
|
||||
KnockServlet(hs).register(http_server)
|
||||
|
|
Loading…
Reference in a new issue