Move lookup-related functions from RoomMemberHandler to IdentityHandler (#5978)
Just to have all the methods that make calls to identity services in one place.
This commit is contained in:
parent
67ee18daea
commit
8c27bc8b60
|
@ -0,0 +1 @@
|
||||||
|
Move lookup-related functions from RoomMemberHandler to IdentityHandler.
|
|
@ -21,11 +21,15 @@ import logging
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
from canonicaljson import json
|
from canonicaljson import json
|
||||||
|
from signedjson.key import decode_verify_key_bytes
|
||||||
|
from signedjson.sign import verify_signed_json
|
||||||
|
from unpaddedbase64 import decode_base64
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.internet.error import TimeoutError
|
from twisted.internet.error import TimeoutError
|
||||||
|
|
||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
|
AuthError,
|
||||||
CodeMessageException,
|
CodeMessageException,
|
||||||
Codes,
|
Codes,
|
||||||
HttpResponseException,
|
HttpResponseException,
|
||||||
|
@ -33,12 +37,15 @@ from synapse.api.errors import (
|
||||||
)
|
)
|
||||||
from synapse.config.emailconfig import ThreepidBehaviour
|
from synapse.config.emailconfig import ThreepidBehaviour
|
||||||
from synapse.http.client import SimpleHttpClient
|
from synapse.http.client import SimpleHttpClient
|
||||||
|
from synapse.util.hash import sha256_and_url_safe_base64
|
||||||
from synapse.util.stringutils import random_string
|
from synapse.util.stringutils import random_string
|
||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
id_server_scheme = "https://"
|
||||||
|
|
||||||
|
|
||||||
class IdentityHandler(BaseHandler):
|
class IdentityHandler(BaseHandler):
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
|
@ -557,6 +564,352 @@ class IdentityHandler(BaseHandler):
|
||||||
logger.warning("Error contacting msisdn account_threepid_delegate: %s", e)
|
logger.warning("Error contacting msisdn account_threepid_delegate: %s", e)
|
||||||
raise SynapseError(400, "Error contacting the identity server")
|
raise SynapseError(400, "Error contacting the identity server")
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def lookup_3pid(self, id_server, medium, address, id_access_token=None):
|
||||||
|
"""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").
|
||||||
|
id_access_token (str|None): The access token to authenticate to the identity
|
||||||
|
server with
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str|None: the matrix ID of the 3pid, or None if it is not recognized.
|
||||||
|
"""
|
||||||
|
if id_access_token is not None:
|
||||||
|
try:
|
||||||
|
results = yield self._lookup_3pid_v2(
|
||||||
|
id_server, id_access_token, medium, address
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Catch HttpResponseExcept for a non-200 response code
|
||||||
|
# Check if this identity server does not know about v2 lookups
|
||||||
|
if isinstance(e, HttpResponseException) and e.code == 404:
|
||||||
|
# This is an old identity server that does not yet support v2 lookups
|
||||||
|
logger.warning(
|
||||||
|
"Attempted v2 lookup on v1 identity server %s. Falling "
|
||||||
|
"back to v1",
|
||||||
|
id_server,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning("Error when looking up hashing details: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return (yield self._lookup_3pid_v1(id_server, medium, address))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _lookup_3pid_v1(self, id_server, medium, address):
|
||||||
|
"""Looks up a 3pid in the passed identity server using v1 lookup.
|
||||||
|
|
||||||
|
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.blacklisting_http_client.get_json(
|
||||||
|
"%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, id_server),
|
||||||
|
{"medium": medium, "address": address},
|
||||||
|
)
|
||||||
|
|
||||||
|
if "mxid" in data:
|
||||||
|
if "signatures" not in data:
|
||||||
|
raise AuthError(401, "No signatures on 3pid binding")
|
||||||
|
yield self._verify_any_signature(data, id_server)
|
||||||
|
return data["mxid"]
|
||||||
|
except TimeoutError:
|
||||||
|
raise SynapseError(500, "Timed out contacting identity server")
|
||||||
|
except IOError as e:
|
||||||
|
logger.warning("Error from v1 identity server lookup: %s" % (e,))
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _lookup_3pid_v2(self, id_server, id_access_token, medium, address):
|
||||||
|
"""Looks up a 3pid in the passed identity server using v2 lookup.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id_server (str): The server name (including port, if required)
|
||||||
|
of the identity server to use.
|
||||||
|
id_access_token (str): The access token to authenticate to the identity server with
|
||||||
|
medium (str): The type of the third party identifier (e.g. "email").
|
||||||
|
address (str): The third party identifier (e.g. "foo@example.com").
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Deferred[str|None]: the matrix ID of the 3pid, or None if it is not recognised.
|
||||||
|
"""
|
||||||
|
# Check what hashing details are supported by this identity server
|
||||||
|
try:
|
||||||
|
hash_details = yield self.blacklisting_http_client.get_json(
|
||||||
|
"%s%s/_matrix/identity/v2/hash_details" % (id_server_scheme, id_server),
|
||||||
|
{"access_token": id_access_token},
|
||||||
|
)
|
||||||
|
except TimeoutError:
|
||||||
|
raise SynapseError(500, "Timed out contacting identity server")
|
||||||
|
|
||||||
|
if not isinstance(hash_details, dict):
|
||||||
|
logger.warning(
|
||||||
|
"Got non-dict object when checking hash details of %s%s: %s",
|
||||||
|
id_server_scheme,
|
||||||
|
id_server,
|
||||||
|
hash_details,
|
||||||
|
)
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Non-dict object from %s%s during v2 hash_details request: %s"
|
||||||
|
% (id_server_scheme, id_server, hash_details),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract information from hash_details
|
||||||
|
supported_lookup_algorithms = hash_details.get("algorithms")
|
||||||
|
lookup_pepper = hash_details.get("lookup_pepper")
|
||||||
|
if (
|
||||||
|
not supported_lookup_algorithms
|
||||||
|
or not isinstance(supported_lookup_algorithms, list)
|
||||||
|
or not lookup_pepper
|
||||||
|
or not isinstance(lookup_pepper, str)
|
||||||
|
):
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Invalid hash details received from identity server %s%s: %s"
|
||||||
|
% (id_server_scheme, id_server, hash_details),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if any of the supported lookup algorithms are present
|
||||||
|
if LookupAlgorithm.SHA256 in supported_lookup_algorithms:
|
||||||
|
# Perform a hashed lookup
|
||||||
|
lookup_algorithm = LookupAlgorithm.SHA256
|
||||||
|
|
||||||
|
# Hash address, medium and the pepper with sha256
|
||||||
|
to_hash = "%s %s %s" % (address, medium, lookup_pepper)
|
||||||
|
lookup_value = sha256_and_url_safe_base64(to_hash)
|
||||||
|
|
||||||
|
elif LookupAlgorithm.NONE in supported_lookup_algorithms:
|
||||||
|
# Perform a non-hashed lookup
|
||||||
|
lookup_algorithm = LookupAlgorithm.NONE
|
||||||
|
|
||||||
|
# Combine together plaintext address and medium
|
||||||
|
lookup_value = "%s %s" % (address, medium)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"None of the provided lookup algorithms of %s are supported: %s",
|
||||||
|
id_server,
|
||||||
|
supported_lookup_algorithms,
|
||||||
|
)
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Provided identity server does not support any v2 lookup "
|
||||||
|
"algorithms that this homeserver supports.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Authenticate with identity server given the access token from the client
|
||||||
|
headers = {"Authorization": create_id_access_token_header(id_access_token)}
|
||||||
|
|
||||||
|
try:
|
||||||
|
lookup_results = yield self.blacklisting_http_client.post_json_get_json(
|
||||||
|
"%s%s/_matrix/identity/v2/lookup" % (id_server_scheme, id_server),
|
||||||
|
{
|
||||||
|
"addresses": [lookup_value],
|
||||||
|
"algorithm": lookup_algorithm,
|
||||||
|
"pepper": lookup_pepper,
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
except TimeoutError:
|
||||||
|
raise SynapseError(500, "Timed out contacting identity server")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Error when performing a v2 3pid lookup: %s", e)
|
||||||
|
raise SynapseError(
|
||||||
|
500, "Unknown error occurred during identity server lookup"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for a mapping from what we looked up to an MXID
|
||||||
|
if "mappings" not in lookup_results or not isinstance(
|
||||||
|
lookup_results["mappings"], dict
|
||||||
|
):
|
||||||
|
logger.warning("No results from 3pid lookup")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Return the MXID if it's available, or None otherwise
|
||||||
|
mxid = lookup_results["mappings"].get(lookup_value)
|
||||||
|
return mxid
|
||||||
|
|
||||||
|
@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():
|
||||||
|
try:
|
||||||
|
key_data = yield self.blacklisting_http_client.get_json(
|
||||||
|
"%s%s/_matrix/identity/api/v1/pubkey/%s"
|
||||||
|
% (id_server_scheme, server_hostname, key_name)
|
||||||
|
)
|
||||||
|
except TimeoutError:
|
||||||
|
raise SynapseError(500, "Timed out contacting identity server")
|
||||||
|
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 ask_id_server_for_third_party_invite(
|
||||||
|
self,
|
||||||
|
requester,
|
||||||
|
id_server,
|
||||||
|
medium,
|
||||||
|
address,
|
||||||
|
room_id,
|
||||||
|
inviter_user_id,
|
||||||
|
room_alias,
|
||||||
|
room_avatar_url,
|
||||||
|
room_join_rules,
|
||||||
|
room_name,
|
||||||
|
inviter_display_name,
|
||||||
|
inviter_avatar_url,
|
||||||
|
id_access_token=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Asks an identity server for a third party invite.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
requester (Requester)
|
||||||
|
id_server (str): hostname + optional port for the identity server.
|
||||||
|
medium (str): The literal string "email".
|
||||||
|
address (str): The third party address being invited.
|
||||||
|
room_id (str): The ID of the room to which the user is invited.
|
||||||
|
inviter_user_id (str): The user ID of the inviter.
|
||||||
|
room_alias (str): An alias for the room, for cosmetic notifications.
|
||||||
|
room_avatar_url (str): The URL of the room's avatar, for cosmetic
|
||||||
|
notifications.
|
||||||
|
room_join_rules (str): The join rules of the email (e.g. "public").
|
||||||
|
room_name (str): The m.room.name of the room.
|
||||||
|
inviter_display_name (str): The current display name of the
|
||||||
|
inviter.
|
||||||
|
inviter_avatar_url (str): The URL of the inviter's avatar.
|
||||||
|
id_access_token (str|None): The access token to authenticate to the identity
|
||||||
|
server with
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A deferred tuple containing:
|
||||||
|
token (str): The token which must be signed to prove authenticity.
|
||||||
|
public_keys ([{"public_key": str, "key_validity_url": str}]):
|
||||||
|
public_key is a base64-encoded ed25519 public key.
|
||||||
|
fallback_public_key: One element from public_keys.
|
||||||
|
display_name (str): A user-friendly name to represent the invited
|
||||||
|
user.
|
||||||
|
"""
|
||||||
|
invite_config = {
|
||||||
|
"medium": medium,
|
||||||
|
"address": address,
|
||||||
|
"room_id": room_id,
|
||||||
|
"room_alias": room_alias,
|
||||||
|
"room_avatar_url": room_avatar_url,
|
||||||
|
"room_join_rules": room_join_rules,
|
||||||
|
"room_name": room_name,
|
||||||
|
"sender": inviter_user_id,
|
||||||
|
"sender_display_name": inviter_display_name,
|
||||||
|
"sender_avatar_url": inviter_avatar_url,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add the identity service access token to the JSON body and use the v2
|
||||||
|
# Identity Service endpoints if id_access_token is present
|
||||||
|
data = None
|
||||||
|
base_url = "%s%s/_matrix/identity" % (id_server_scheme, id_server)
|
||||||
|
|
||||||
|
if id_access_token:
|
||||||
|
key_validity_url = "%s%s/_matrix/identity/v2/pubkey/isvalid" % (
|
||||||
|
id_server_scheme,
|
||||||
|
id_server,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attempt a v2 lookup
|
||||||
|
url = base_url + "/v2/store-invite"
|
||||||
|
try:
|
||||||
|
data = yield self.blacklisting_http_client.post_json_get_json(
|
||||||
|
url,
|
||||||
|
invite_config,
|
||||||
|
{"Authorization": create_id_access_token_header(id_access_token)},
|
||||||
|
)
|
||||||
|
except TimeoutError:
|
||||||
|
raise SynapseError(500, "Timed out contacting identity server")
|
||||||
|
except HttpResponseException as e:
|
||||||
|
if e.code != 404:
|
||||||
|
logger.info("Failed to POST %s with JSON: %s", url, e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
key_validity_url = "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % (
|
||||||
|
id_server_scheme,
|
||||||
|
id_server,
|
||||||
|
)
|
||||||
|
url = base_url + "/api/v1/store-invite"
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = yield self.blacklisting_http_client.post_json_get_json(
|
||||||
|
url, invite_config
|
||||||
|
)
|
||||||
|
except TimeoutError:
|
||||||
|
raise SynapseError(500, "Timed out contacting identity server")
|
||||||
|
except HttpResponseException as e:
|
||||||
|
logger.warning(
|
||||||
|
"Error trying to call /store-invite on %s%s: %s",
|
||||||
|
id_server_scheme,
|
||||||
|
id_server,
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
# Some identity servers may only support application/x-www-form-urlencoded
|
||||||
|
# types. This is especially true with old instances of Sydent, see
|
||||||
|
# https://github.com/matrix-org/sydent/pull/170
|
||||||
|
try:
|
||||||
|
data = yield self.blacklisting_http_client.post_urlencoded_get_json(
|
||||||
|
url, invite_config
|
||||||
|
)
|
||||||
|
except HttpResponseException as e:
|
||||||
|
logger.warning(
|
||||||
|
"Error calling /store-invite on %s%s with fallback "
|
||||||
|
"encoding: %s",
|
||||||
|
id_server_scheme,
|
||||||
|
id_server,
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# TODO: Check for success
|
||||||
|
token = data["token"]
|
||||||
|
public_keys = data.get("public_keys", [])
|
||||||
|
if "public_key" in data:
|
||||||
|
fallback_public_key = {
|
||||||
|
"public_key": data["public_key"],
|
||||||
|
"key_validity_url": key_validity_url,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
fallback_public_key = public_keys[0]
|
||||||
|
|
||||||
|
if not public_keys:
|
||||||
|
public_keys.append(fallback_public_key)
|
||||||
|
display_name = data["display_name"]
|
||||||
|
return token, public_keys, fallback_public_key, display_name
|
||||||
|
|
||||||
|
|
||||||
def create_id_access_token_header(id_access_token):
|
def create_id_access_token_header(id_access_token):
|
||||||
"""Create an Authorization header for passing to SimpleHttpClient as the header value
|
"""Create an Authorization header for passing to SimpleHttpClient as the header value
|
||||||
|
|
|
@ -20,29 +20,19 @@ import logging
|
||||||
|
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
|
|
||||||
from signedjson.key import decode_verify_key_bytes
|
|
||||||
from signedjson.sign import verify_signed_json
|
|
||||||
from unpaddedbase64 import decode_base64
|
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.internet.error import TimeoutError
|
|
||||||
|
|
||||||
from synapse import types
|
from synapse import types
|
||||||
from synapse.api.constants import EventTypes, Membership
|
from synapse.api.constants import EventTypes, Membership
|
||||||
from synapse.api.errors import AuthError, Codes, HttpResponseException, SynapseError
|
from synapse.api.errors import AuthError, Codes, SynapseError
|
||||||
from synapse.handlers.identity import LookupAlgorithm, create_id_access_token_header
|
|
||||||
from synapse.http.client import SimpleHttpClient
|
|
||||||
from synapse.types import RoomID, UserID
|
from synapse.types import RoomID, UserID
|
||||||
from synapse.util.async_helpers import Linearizer
|
from synapse.util.async_helpers import Linearizer
|
||||||
from synapse.util.distributor import user_joined_room, user_left_room
|
from synapse.util.distributor import user_joined_room, user_left_room
|
||||||
from synapse.util.hash import sha256_and_url_safe_base64
|
|
||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
id_server_scheme = "https://"
|
|
||||||
|
|
||||||
|
|
||||||
class RoomMemberHandler(object):
|
class RoomMemberHandler(object):
|
||||||
# TODO(paul): This handler currently contains a messy conflation of
|
# TODO(paul): This handler currently contains a messy conflation of
|
||||||
|
@ -63,14 +53,10 @@ class RoomMemberHandler(object):
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
self.state_handler = hs.get_state_handler()
|
self.state_handler = hs.get_state_handler()
|
||||||
self.config = hs.config
|
self.config = hs.config
|
||||||
# We create a blacklisting instance of SimpleHttpClient for contacting identity
|
|
||||||
# servers specified by clients
|
|
||||||
self.simple_http_client = SimpleHttpClient(
|
|
||||||
hs, ip_blacklist=hs.config.federation_ip_range_blacklist
|
|
||||||
)
|
|
||||||
|
|
||||||
self.federation_handler = hs.get_handlers().federation_handler
|
self.federation_handler = hs.get_handlers().federation_handler
|
||||||
self.directory_handler = hs.get_handlers().directory_handler
|
self.directory_handler = hs.get_handlers().directory_handler
|
||||||
|
self.identity_handler = hs.get_handlers().identity_handler
|
||||||
self.registration_handler = hs.get_registration_handler()
|
self.registration_handler = hs.get_registration_handler()
|
||||||
self.profile_handler = hs.get_profile_handler()
|
self.profile_handler = hs.get_profile_handler()
|
||||||
self.event_creation_handler = hs.get_event_creation_handler()
|
self.event_creation_handler = hs.get_event_creation_handler()
|
||||||
|
@ -682,7 +668,9 @@ class RoomMemberHandler(object):
|
||||||
403, "Looking up third-party identifiers is denied from this server"
|
403, "Looking up third-party identifiers is denied from this server"
|
||||||
)
|
)
|
||||||
|
|
||||||
invitee = yield self._lookup_3pid(id_server, medium, address, id_access_token)
|
invitee = yield self.identity_handler.lookup_3pid(
|
||||||
|
id_server, medium, address, id_access_token
|
||||||
|
)
|
||||||
|
|
||||||
if invitee:
|
if invitee:
|
||||||
yield self.update_membership(
|
yield self.update_membership(
|
||||||
|
@ -700,211 +688,6 @@ class RoomMemberHandler(object):
|
||||||
id_access_token=id_access_token,
|
id_access_token=id_access_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _lookup_3pid(self, id_server, medium, address, id_access_token=None):
|
|
||||||
"""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").
|
|
||||||
id_access_token (str|None): The access token to authenticate to the identity
|
|
||||||
server with
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str|None: the matrix ID of the 3pid, or None if it is not recognized.
|
|
||||||
"""
|
|
||||||
if id_access_token is not None:
|
|
||||||
try:
|
|
||||||
results = yield self._lookup_3pid_v2(
|
|
||||||
id_server, id_access_token, medium, address
|
|
||||||
)
|
|
||||||
return results
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# Catch HttpResponseExcept for a non-200 response code
|
|
||||||
# Check if this identity server does not know about v2 lookups
|
|
||||||
if isinstance(e, HttpResponseException) and e.code == 404:
|
|
||||||
# This is an old identity server that does not yet support v2 lookups
|
|
||||||
logger.warning(
|
|
||||||
"Attempted v2 lookup on v1 identity server %s. Falling "
|
|
||||||
"back to v1",
|
|
||||||
id_server,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warning("Error when looking up hashing details: %s", e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
return (yield self._lookup_3pid_v1(id_server, medium, address))
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _lookup_3pid_v1(self, id_server, medium, address):
|
|
||||||
"""Looks up a 3pid in the passed identity server using v1 lookup.
|
|
||||||
|
|
||||||
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.simple_http_client.get_json(
|
|
||||||
"%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, id_server),
|
|
||||||
{"medium": medium, "address": address},
|
|
||||||
)
|
|
||||||
|
|
||||||
if "mxid" in data:
|
|
||||||
if "signatures" not in data:
|
|
||||||
raise AuthError(401, "No signatures on 3pid binding")
|
|
||||||
yield self._verify_any_signature(data, id_server)
|
|
||||||
return data["mxid"]
|
|
||||||
except TimeoutError:
|
|
||||||
raise SynapseError(500, "Timed out contacting identity server")
|
|
||||||
except IOError as e:
|
|
||||||
logger.warning("Error from v1 identity server lookup: %s" % (e,))
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _lookup_3pid_v2(self, id_server, id_access_token, medium, address):
|
|
||||||
"""Looks up a 3pid in the passed identity server using v2 lookup.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
id_server (str): The server name (including port, if required)
|
|
||||||
of the identity server to use.
|
|
||||||
id_access_token (str): The access token to authenticate to the identity server with
|
|
||||||
medium (str): The type of the third party identifier (e.g. "email").
|
|
||||||
address (str): The third party identifier (e.g. "foo@example.com").
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Deferred[str|None]: the matrix ID of the 3pid, or None if it is not recognised.
|
|
||||||
"""
|
|
||||||
# Check what hashing details are supported by this identity server
|
|
||||||
try:
|
|
||||||
hash_details = yield self.simple_http_client.get_json(
|
|
||||||
"%s%s/_matrix/identity/v2/hash_details" % (id_server_scheme, id_server),
|
|
||||||
{"access_token": id_access_token},
|
|
||||||
)
|
|
||||||
except TimeoutError:
|
|
||||||
raise SynapseError(500, "Timed out contacting identity server")
|
|
||||||
|
|
||||||
if not isinstance(hash_details, dict):
|
|
||||||
logger.warning(
|
|
||||||
"Got non-dict object when checking hash details of %s%s: %s",
|
|
||||||
id_server_scheme,
|
|
||||||
id_server,
|
|
||||||
hash_details,
|
|
||||||
)
|
|
||||||
raise SynapseError(
|
|
||||||
400,
|
|
||||||
"Non-dict object from %s%s during v2 hash_details request: %s"
|
|
||||||
% (id_server_scheme, id_server, hash_details),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract information from hash_details
|
|
||||||
supported_lookup_algorithms = hash_details.get("algorithms")
|
|
||||||
lookup_pepper = hash_details.get("lookup_pepper")
|
|
||||||
if (
|
|
||||||
not supported_lookup_algorithms
|
|
||||||
or not isinstance(supported_lookup_algorithms, list)
|
|
||||||
or not lookup_pepper
|
|
||||||
or not isinstance(lookup_pepper, str)
|
|
||||||
):
|
|
||||||
raise SynapseError(
|
|
||||||
400,
|
|
||||||
"Invalid hash details received from identity server %s%s: %s"
|
|
||||||
% (id_server_scheme, id_server, hash_details),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if any of the supported lookup algorithms are present
|
|
||||||
if LookupAlgorithm.SHA256 in supported_lookup_algorithms:
|
|
||||||
# Perform a hashed lookup
|
|
||||||
lookup_algorithm = LookupAlgorithm.SHA256
|
|
||||||
|
|
||||||
# Hash address, medium and the pepper with sha256
|
|
||||||
to_hash = "%s %s %s" % (address, medium, lookup_pepper)
|
|
||||||
lookup_value = sha256_and_url_safe_base64(to_hash)
|
|
||||||
|
|
||||||
elif LookupAlgorithm.NONE in supported_lookup_algorithms:
|
|
||||||
# Perform a non-hashed lookup
|
|
||||||
lookup_algorithm = LookupAlgorithm.NONE
|
|
||||||
|
|
||||||
# Combine together plaintext address and medium
|
|
||||||
lookup_value = "%s %s" % (address, medium)
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
"None of the provided lookup algorithms of %s are supported: %s",
|
|
||||||
id_server,
|
|
||||||
supported_lookup_algorithms,
|
|
||||||
)
|
|
||||||
raise SynapseError(
|
|
||||||
400,
|
|
||||||
"Provided identity server does not support any v2 lookup "
|
|
||||||
"algorithms that this homeserver supports.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Authenticate with identity server given the access token from the client
|
|
||||||
headers = {"Authorization": create_id_access_token_header(id_access_token)}
|
|
||||||
|
|
||||||
try:
|
|
||||||
lookup_results = yield self.simple_http_client.post_json_get_json(
|
|
||||||
"%s%s/_matrix/identity/v2/lookup" % (id_server_scheme, id_server),
|
|
||||||
{
|
|
||||||
"addresses": [lookup_value],
|
|
||||||
"algorithm": lookup_algorithm,
|
|
||||||
"pepper": lookup_pepper,
|
|
||||||
},
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
except TimeoutError:
|
|
||||||
raise SynapseError(500, "Timed out contacting identity server")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning("Error when performing a v2 3pid lookup: %s", e)
|
|
||||||
raise SynapseError(
|
|
||||||
500, "Unknown error occurred during identity server lookup"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for a mapping from what we looked up to an MXID
|
|
||||||
if "mappings" not in lookup_results or not isinstance(
|
|
||||||
lookup_results["mappings"], dict
|
|
||||||
):
|
|
||||||
logger.warning("No results from 3pid lookup")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Return the MXID if it's available, or None otherwise
|
|
||||||
mxid = lookup_results["mappings"].get(lookup_value)
|
|
||||||
return mxid
|
|
||||||
|
|
||||||
@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():
|
|
||||||
try:
|
|
||||||
key_data = yield self.simple_http_client.get_json(
|
|
||||||
"%s%s/_matrix/identity/api/v1/pubkey/%s"
|
|
||||||
% (id_server_scheme, server_hostname, key_name)
|
|
||||||
)
|
|
||||||
except TimeoutError:
|
|
||||||
raise SynapseError(500, "Timed out contacting identity server")
|
|
||||||
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
|
@defer.inlineCallbacks
|
||||||
def _make_and_store_3pid_invite(
|
def _make_and_store_3pid_invite(
|
||||||
self,
|
self,
|
||||||
|
@ -951,7 +734,7 @@ class RoomMemberHandler(object):
|
||||||
room_avatar_url = room_avatar_event.content.get("url", "")
|
room_avatar_url = room_avatar_event.content.get("url", "")
|
||||||
|
|
||||||
token, public_keys, fallback_public_key, display_name = (
|
token, public_keys, fallback_public_key, display_name = (
|
||||||
yield self._ask_id_server_for_third_party_invite(
|
yield self.identity_handler.ask_id_server_for_third_party_invite(
|
||||||
requester=requester,
|
requester=requester,
|
||||||
id_server=id_server,
|
id_server=id_server,
|
||||||
medium=medium,
|
medium=medium,
|
||||||
|
@ -987,147 +770,6 @@ class RoomMemberHandler(object):
|
||||||
txn_id=txn_id,
|
txn_id=txn_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _ask_id_server_for_third_party_invite(
|
|
||||||
self,
|
|
||||||
requester,
|
|
||||||
id_server,
|
|
||||||
medium,
|
|
||||||
address,
|
|
||||||
room_id,
|
|
||||||
inviter_user_id,
|
|
||||||
room_alias,
|
|
||||||
room_avatar_url,
|
|
||||||
room_join_rules,
|
|
||||||
room_name,
|
|
||||||
inviter_display_name,
|
|
||||||
inviter_avatar_url,
|
|
||||||
id_access_token=None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Asks an identity server for a third party invite.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
requester (Requester)
|
|
||||||
id_server (str): hostname + optional port for the identity server.
|
|
||||||
medium (str): The literal string "email".
|
|
||||||
address (str): The third party address being invited.
|
|
||||||
room_id (str): The ID of the room to which the user is invited.
|
|
||||||
inviter_user_id (str): The user ID of the inviter.
|
|
||||||
room_alias (str): An alias for the room, for cosmetic notifications.
|
|
||||||
room_avatar_url (str): The URL of the room's avatar, for cosmetic
|
|
||||||
notifications.
|
|
||||||
room_join_rules (str): The join rules of the email (e.g. "public").
|
|
||||||
room_name (str): The m.room.name of the room.
|
|
||||||
inviter_display_name (str): The current display name of the
|
|
||||||
inviter.
|
|
||||||
inviter_avatar_url (str): The URL of the inviter's avatar.
|
|
||||||
id_access_token (str|None): The access token to authenticate to the identity
|
|
||||||
server with
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A deferred tuple containing:
|
|
||||||
token (str): The token which must be signed to prove authenticity.
|
|
||||||
public_keys ([{"public_key": str, "key_validity_url": str}]):
|
|
||||||
public_key is a base64-encoded ed25519 public key.
|
|
||||||
fallback_public_key: One element from public_keys.
|
|
||||||
display_name (str): A user-friendly name to represent the invited
|
|
||||||
user.
|
|
||||||
"""
|
|
||||||
invite_config = {
|
|
||||||
"medium": medium,
|
|
||||||
"address": address,
|
|
||||||
"room_id": room_id,
|
|
||||||
"room_alias": room_alias,
|
|
||||||
"room_avatar_url": room_avatar_url,
|
|
||||||
"room_join_rules": room_join_rules,
|
|
||||||
"room_name": room_name,
|
|
||||||
"sender": inviter_user_id,
|
|
||||||
"sender_display_name": inviter_display_name,
|
|
||||||
"sender_avatar_url": inviter_avatar_url,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add the identity service access token to the JSON body and use the v2
|
|
||||||
# Identity Service endpoints if id_access_token is present
|
|
||||||
data = None
|
|
||||||
base_url = "%s%s/_matrix/identity" % (id_server_scheme, id_server)
|
|
||||||
|
|
||||||
if id_access_token:
|
|
||||||
key_validity_url = "%s%s/_matrix/identity/v2/pubkey/isvalid" % (
|
|
||||||
id_server_scheme,
|
|
||||||
id_server,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Attempt a v2 lookup
|
|
||||||
url = base_url + "/v2/store-invite"
|
|
||||||
try:
|
|
||||||
data = yield self.simple_http_client.post_json_get_json(
|
|
||||||
url,
|
|
||||||
invite_config,
|
|
||||||
{"Authorization": create_id_access_token_header(id_access_token)},
|
|
||||||
)
|
|
||||||
except TimeoutError:
|
|
||||||
raise SynapseError(500, "Timed out contacting identity server")
|
|
||||||
except HttpResponseException as e:
|
|
||||||
if e.code != 404:
|
|
||||||
logger.info("Failed to POST %s with JSON: %s", url, e)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
if data is None:
|
|
||||||
key_validity_url = "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % (
|
|
||||||
id_server_scheme,
|
|
||||||
id_server,
|
|
||||||
)
|
|
||||||
url = base_url + "/api/v1/store-invite"
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = yield self.simple_http_client.post_json_get_json(
|
|
||||||
url, invite_config
|
|
||||||
)
|
|
||||||
except TimeoutError:
|
|
||||||
raise SynapseError(500, "Timed out contacting identity server")
|
|
||||||
except HttpResponseException as e:
|
|
||||||
logger.warning(
|
|
||||||
"Error trying to call /store-invite on %s%s: %s",
|
|
||||||
id_server_scheme,
|
|
||||||
id_server,
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
|
|
||||||
if data is None:
|
|
||||||
# Some identity servers may only support application/x-www-form-urlencoded
|
|
||||||
# types. This is especially true with old instances of Sydent, see
|
|
||||||
# https://github.com/matrix-org/sydent/pull/170
|
|
||||||
try:
|
|
||||||
data = yield self.simple_http_client.post_urlencoded_get_json(
|
|
||||||
url, invite_config
|
|
||||||
)
|
|
||||||
except HttpResponseException as e:
|
|
||||||
logger.warning(
|
|
||||||
"Error calling /store-invite on %s%s with fallback "
|
|
||||||
"encoding: %s",
|
|
||||||
id_server_scheme,
|
|
||||||
id_server,
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
# TODO: Check for success
|
|
||||||
token = data["token"]
|
|
||||||
public_keys = data.get("public_keys", [])
|
|
||||||
if "public_key" in data:
|
|
||||||
fallback_public_key = {
|
|
||||||
"public_key": data["public_key"],
|
|
||||||
"key_validity_url": key_validity_url,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
fallback_public_key = public_keys[0]
|
|
||||||
|
|
||||||
if not public_keys:
|
|
||||||
public_keys.append(fallback_public_key)
|
|
||||||
display_name = data["display_name"]
|
|
||||||
return token, public_keys, fallback_public_key, display_name
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _is_host_in_room(self, current_state_ids):
|
def _is_host_in_room(self, current_state_ids):
|
||||||
# Have we just created the room, and is this about to be the very
|
# Have we just created the room, and is this about to be the very
|
||||||
|
|
Loading…
Reference in New Issue