diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index 3364a5de14..60f9fa58b0 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -22,11 +22,16 @@ from synapse.types import UserID, RoomAlias, RoomID
from synapse.api.constants import (
EventTypes, Membership, JoinRules, RoomCreationPreset,
)
-from synapse.api.errors import StoreError, SynapseError
+from synapse.api.errors import AuthError, StoreError, SynapseError
from synapse.util import stringutils, unwrapFirstError
from synapse.util.async import run_on_reactor
+from signedjson.sign import verify_signed_json
+from signedjson.key import decode_verify_key_bytes
+
from collections import OrderedDict
+from unpaddedbase64 import decode_base64
+
import logging
import string
@@ -384,7 +389,22 @@ class RoomMemberHandler(BaseHandler):
if event.membership == Membership.JOIN:
yield self._do_join(event, context, do_auth=do_auth)
else:
- # This is not a JOIN, so we can handle it normally.
+ if event.membership == Membership.LEAVE:
+ is_host_in_room = yield self.is_host_in_room(room_id, context)
+ if not is_host_in_room:
+ # Rejecting an invite, rather than leaving a joined room
+ handler = self.hs.get_handlers().federation_handler
+ inviter = yield self.get_inviter(event)
+ if not inviter:
+ # return the same error as join_room_alias does
+ raise SynapseError(404, "No known servers")
+ yield handler.do_remotely_reject_invite(
+ [inviter.domain],
+ room_id,
+ event.user_id
+ )
+ defer.returnValue({"room_id": room_id})
+ return
# FIXME: This isn't idempotency.
if prev_state and prev_state.membership == event.membership:
@@ -408,7 +428,7 @@ class RoomMemberHandler(BaseHandler):
defer.returnValue({"room_id": room_id})
@defer.inlineCallbacks
- def join_room_alias(self, joinee, room_alias, do_auth=True, content={}):
+ def join_room_alias(self, joinee, room_alias, content={}):
directory_handler = self.hs.get_handlers().directory_handler
mapping = yield directory_handler.get_association(room_alias)
@@ -442,8 +462,6 @@ class RoomMemberHandler(BaseHandler):
@defer.inlineCallbacks
def _do_join(self, event, context, room_hosts=None, do_auth=True):
- joinee = UserID.from_string(event.state_key)
- # room_id = RoomID.from_string(event.room_id, self.hs)
room_id = event.room_id
# XXX: We don't do an auth check if we are doing an invite
@@ -451,41 +469,18 @@ class RoomMemberHandler(BaseHandler):
# that we are allowed to join when we decide whether or not we
# need to do the invite/join dance.
- is_host_in_room = yield self.auth.check_host_in_room(
- event.room_id,
- self.hs.hostname
- )
- if not is_host_in_room:
- # is *anyone* in the room?
- room_member_keys = [
- v for (k, v) in context.current_state.keys() if (
- k == "m.room.member"
- )
- ]
- if len(room_member_keys) == 0:
- # has the room been created so we can join it?
- create_event = context.current_state.get(("m.room.create", ""))
- if create_event:
- is_host_in_room = True
-
+ is_host_in_room = yield self.is_host_in_room(room_id, context)
if is_host_in_room:
should_do_dance = False
elif room_hosts: # TODO: Shouldn't this be remote_room_host?
should_do_dance = True
else:
- # TODO(markjh): get prev_state from snapshot
- prev_state = yield self.store.get_room_member(
- joinee.to_string(), room_id
- )
-
- if prev_state and prev_state.membership == Membership.INVITE:
- inviter = UserID.from_string(prev_state.user_id)
-
- should_do_dance = not self.hs.is_mine(inviter)
- room_hosts = [inviter.domain]
- else:
+ inviter = yield self.get_inviter(event)
+ if not inviter:
# return the same error as join_room_alias does
raise SynapseError(404, "No known servers")
+ should_do_dance = not self.hs.is_mine(inviter)
+ room_hosts = [inviter.domain]
if should_do_dance:
handler = self.hs.get_handlers().federation_handler
@@ -493,8 +488,7 @@ class RoomMemberHandler(BaseHandler):
room_hosts,
room_id,
event.user_id,
- event.content, # FIXME To get a non-frozen dict
- context
+ event.content # FIXME To get a non-frozen dict
)
else:
logger.debug("Doing normal join")
@@ -512,6 +506,44 @@ class RoomMemberHandler(BaseHandler):
)
@defer.inlineCallbacks
+ def get_inviter(self, event):
+ # TODO(markjh): get prev_state from snapshot
+ prev_state = yield self.store.get_room_member(
+ event.user_id, event.room_id
+ )
+
+ if prev_state and prev_state.membership == Membership.INVITE:
+ defer.returnValue(UserID.from_string(prev_state.user_id))
+ return
+ elif "third_party_invite" in event.content:
+ if "sender" in event.content["third_party_invite"]:
+ inviter = UserID.from_string(
+ event.content["third_party_invite"]["sender"]
+ )
+ defer.returnValue(inviter)
+ defer.returnValue(None)
+
+ @defer.inlineCallbacks
+ def is_host_in_room(self, room_id, context):
+ is_host_in_room = yield self.auth.check_host_in_room(
+ room_id,
+ self.hs.hostname
+ )
+ if not is_host_in_room:
+ # is *anyone* in the room?
+ room_member_keys = [
+ v for (k, v) in context.current_state.keys() if (
+ k == "m.room.member"
+ )
+ ]
+ if len(room_member_keys) == 0:
+ # has the room been created so we can join it?
+ create_event = context.current_state.get(("m.room.create", ""))
+ if create_event:
+ is_host_in_room = True
+ defer.returnValue(is_host_in_room)
+
+ @defer.inlineCallbacks
def get_joined_rooms_for_user(self, user):
"""Returns a list of roomids that the user has any of the given
membership states in."""
@@ -540,6 +572,160 @@ class RoomMemberHandler(BaseHandler):
suppress_auth=(not do_auth),
)
+ @defer.inlineCallbacks
+ def do_3pid_invite(
+ self,
+ room_id,
+ inviter,
+ medium,
+ address,
+ id_server,
+ display_name,
+ token_id,
+ txn_id
+ ):
+ invitee = yield self._lookup_3pid(
+ id_server, medium, address
+ )
+
+ if invitee:
+ # make sure it looks like a user ID; it'll throw if it's invalid.
+ UserID.from_string(invitee)
+ yield self.hs.get_handlers().message_handler.create_and_send_event(
+ {
+ "type": EventTypes.Member,
+ "content": {
+ "membership": unicode("invite")
+ },
+ "room_id": room_id,
+ "sender": inviter.to_string(),
+ "state_key": invitee,
+ },
+ token_id=token_id,
+ txn_id=txn_id,
+ )
+ else:
+ yield self._make_and_store_3pid_invite(
+ id_server,
+ display_name,
+ medium,
+ address,
+ room_id,
+ inviter,
+ token_id,
+ txn_id=txn_id
+ )
+
+ @defer.inlineCallbacks
+ def _lookup_3pid(self, id_server, medium, address):
+ """Looks up a 3pid in the passed identity server.
+
+ Args:
+ id_server (str): The server name (including port, if required)
+ of the identity server to use.
+ medium (str): The type of the third party identifier (e.g. "email").
+ address (str): The third party identifier (e.g. "foo@example.com").
+
+ Returns:
+ (str) the matrix ID of the 3pid, or None if it is not recognized.
+ """
+ try:
+ data = yield self.hs.get_simple_http_client().get_json(
+ "https://%s/_matrix/identity/api/v1/lookup" % (id_server,),
+ {
+ "medium": medium,
+ "address": address,
+ }
+ )
+
+ if "mxid" in data:
+ if "signatures" not in data:
+ raise AuthError(401, "No signatures on 3pid binding")
+ self.verify_any_signature(data, id_server)
+ defer.returnValue(data["mxid"])
+
+ except IOError as e:
+ logger.warn("Error from identity server lookup: %s" % (e,))
+ defer.returnValue(None)
+
+ @defer.inlineCallbacks
+ def verify_any_signature(self, data, server_hostname):
+ if server_hostname not in data["signatures"]:
+ raise AuthError(401, "No signature from server %s" % (server_hostname,))
+ for key_name, signature in data["signatures"][server_hostname].items():
+ key_data = yield self.hs.get_simple_http_client().get_json(
+ "https://%s/_matrix/identity/api/v1/pubkey/%s" %
+ (server_hostname, key_name,),
+ )
+ if "public_key" not in key_data:
+ raise AuthError(401, "No public key named %s from %s" %
+ (key_name, server_hostname,))
+ verify_signed_json(
+ data,
+ server_hostname,
+ decode_verify_key_bytes(key_name, decode_base64(key_data["public_key"]))
+ )
+ return
+
+ @defer.inlineCallbacks
+ def _make_and_store_3pid_invite(
+ self,
+ id_server,
+ display_name,
+ medium,
+ address,
+ room_id,
+ user,
+ token_id,
+ txn_id
+ ):
+ token, public_key, key_validity_url = (
+ yield self._ask_id_server_for_third_party_invite(
+ id_server,
+ medium,
+ address,
+ room_id,
+ user.to_string()
+ )
+ )
+ msg_handler = self.hs.get_handlers().message_handler
+ yield msg_handler.create_and_send_event(
+ {
+ "type": EventTypes.ThirdPartyInvite,
+ "content": {
+ "display_name": display_name,
+ "key_validity_url": key_validity_url,
+ "public_key": public_key,
+ },
+ "room_id": room_id,
+ "sender": user.to_string(),
+ "state_key": token,
+ },
+ token_id=token_id,
+ txn_id=txn_id,
+ )
+
+ @defer.inlineCallbacks
+ def _ask_id_server_for_third_party_invite(
+ self, id_server, medium, address, room_id, sender):
+ is_url = "https://%s/_matrix/identity/api/v1/store-invite" % (id_server,)
+ data = yield self.hs.get_simple_http_client().post_urlencoded_get_json(
+ is_url,
+ {
+ "medium": medium,
+ "address": address,
+ "room_id": room_id,
+ "sender": sender,
+ }
+ )
+ # TODO: Check for success
+ token = data["token"]
+ public_key = data["public_key"]
+ key_validity_url = "https://%s/_matrix/identity/api/v1/pubkey/isvalid" % (
+ id_server,
+ )
+ defer.returnValue((token, public_key, key_validity_url))
+
class RoomListHandler(BaseHandler):
|