draft for federation of knock (untested)

This commit is contained in:
Sorunome 2020-01-19 12:35:59 +01:00
parent 057740f283
commit ddd3584bdc
No known key found for this signature in database
GPG key ID: 63E31F7B5993A9C4
6 changed files with 302 additions and 3 deletions

View file

@ -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,

View file

@ -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):

View file

@ -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>[^/]*)"

View file

@ -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.

View file

@ -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
"""

View file

@ -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)