Use the v2 lookup API for 3PID invites (#5897)
This commit is contained in:
1
changelog.d/5897.feature
Normal file
1
changelog.d/5897.feature
Normal file
@@ -0,0 +1 @@
|
||||
Switch to the v2 lookup API for 3PID invites.
|
||||
@@ -13,7 +13,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
class SpamChecker(object):
|
||||
def __init__(self, hs):
|
||||
self.spam_checker = None
|
||||
|
||||
@@ -20,18 +20,13 @@
|
||||
import logging
|
||||
|
||||
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 synapse.api.errors import (
|
||||
AuthError,
|
||||
CodeMessageException,
|
||||
Codes,
|
||||
HttpResponseException,
|
||||
ProxiedRequestError,
|
||||
SynapseError,
|
||||
)
|
||||
|
||||
@@ -48,26 +43,9 @@ class IdentityHandler(BaseHandler):
|
||||
self.federation_http_client = hs.get_http_client()
|
||||
|
||||
self.trusted_id_servers = set(hs.config.trusted_third_party_id_servers)
|
||||
self.trust_any_id_server_just_for_testing_do_not_use = (
|
||||
hs.config.use_insecure_ssl_client_just_for_testing_do_not_use
|
||||
)
|
||||
self.rewrite_identity_server_urls = hs.config.rewrite_identity_server_urls
|
||||
self._enable_lookup = hs.config.enable_3pid_lookup
|
||||
|
||||
def _should_trust_id_server(self, id_server):
|
||||
if id_server not in self.trusted_id_servers:
|
||||
if self.trust_any_id_server_just_for_testing_do_not_use:
|
||||
logger.warn(
|
||||
"Trusting untrustworthy ID server %r even though it isn't"
|
||||
" in the trusted id list for testing because"
|
||||
" 'use_insecure_ssl_client_just_for_testing_do_not_use'"
|
||||
" is set in the config",
|
||||
id_server,
|
||||
)
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def threepid_from_creds(self, creds):
|
||||
if "id_server" in creds:
|
||||
@@ -84,16 +62,17 @@ class IdentityHandler(BaseHandler):
|
||||
else:
|
||||
raise SynapseError(400, "No client_secret in creds")
|
||||
|
||||
if not self._should_trust_id_server(id_server):
|
||||
if not should_trust_id_server(self.hs, id_server):
|
||||
logger.warn(
|
||||
"%s is not a trusted ID server: rejecting 3pid " + "credentials",
|
||||
id_server,
|
||||
)
|
||||
return None
|
||||
|
||||
# if we have a rewrite rule set for the identity server,
|
||||
# apply it now.
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
id_server = self.rewrite_identity_server_urls[id_server]
|
||||
id_server = self.rewrite_identity_server_urls.get(id_server, id_server)
|
||||
|
||||
try:
|
||||
data = yield self.http_client.get_json(
|
||||
"https://%s%s"
|
||||
@@ -130,10 +109,7 @@ class IdentityHandler(BaseHandler):
|
||||
# if we have a rewrite rule set for the identity server,
|
||||
# apply it now, but only for sending the request (not
|
||||
# storing in the database).
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
id_server_host = self.rewrite_identity_server_urls[id_server]
|
||||
else:
|
||||
id_server_host = id_server
|
||||
id_server_host = self.rewrite_identity_server_urls.get(id_server, id_server)
|
||||
|
||||
try:
|
||||
data = yield self.http_client.post_json_get_json(
|
||||
@@ -205,7 +181,6 @@ class IdentityHandler(BaseHandler):
|
||||
Deferred[bool]: True on success, otherwise False if the identity
|
||||
server doesn't support unbinding
|
||||
"""
|
||||
url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,)
|
||||
content = {
|
||||
"mxid": mxid,
|
||||
"threepid": {"medium": threepid["medium"], "address": threepid["address"]},
|
||||
@@ -228,8 +203,7 @@ class IdentityHandler(BaseHandler):
|
||||
#
|
||||
# Note that destination_is has to be the real id_server, not
|
||||
# the server we connect to.
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
id_server = self.rewrite_identity_server_urls[id_server]
|
||||
id_server = self.rewrite_identity_server_urls.get(id_server, id_server)
|
||||
|
||||
url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,)
|
||||
|
||||
@@ -258,7 +232,7 @@ class IdentityHandler(BaseHandler):
|
||||
def requestEmailToken(
|
||||
self, id_server, email, client_secret, send_attempt, next_link=None
|
||||
):
|
||||
if not self._should_trust_id_server(id_server):
|
||||
if not should_trust_id_server(self.hs, id_server):
|
||||
raise SynapseError(
|
||||
400, "Untrusted ID server '%s'" % id_server, Codes.SERVER_NOT_TRUSTED
|
||||
)
|
||||
@@ -269,10 +243,8 @@ class IdentityHandler(BaseHandler):
|
||||
"send_attempt": send_attempt,
|
||||
}
|
||||
|
||||
# if we have a rewrite rule set for the identity server,
|
||||
# apply it now.
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
id_server = self.rewrite_identity_server_urls[id_server]
|
||||
# Rewrite id_server URL if necessary
|
||||
id_server = self.rewrite_identity_server_urls.get(id_server, id_server)
|
||||
|
||||
if next_link:
|
||||
params.update({"next_link": next_link})
|
||||
@@ -292,11 +264,14 @@ class IdentityHandler(BaseHandler):
|
||||
def requestMsisdnToken(
|
||||
self, id_server, country, phone_number, client_secret, send_attempt, **kwargs
|
||||
):
|
||||
if not self._should_trust_id_server(id_server):
|
||||
if not should_trust_id_server(self.hs, id_server):
|
||||
raise SynapseError(
|
||||
400, "Untrusted ID server '%s'" % id_server, Codes.SERVER_NOT_TRUSTED
|
||||
)
|
||||
|
||||
# Rewrite id_server URL if necessary
|
||||
id_server = self.rewrite_identity_server_urls.get(id_server, id_server)
|
||||
|
||||
params = {
|
||||
"country": country,
|
||||
"phone_number": phone_number,
|
||||
@@ -319,119 +294,30 @@ class IdentityHandler(BaseHandler):
|
||||
logger.info("Proxied requestToken failed: %r", e)
|
||||
raise e.to_synapse_error()
|
||||
|
||||
@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:
|
||||
Deferred[dict]: The result of the lookup. See
|
||||
https://matrix.org/docs/spec/identity_service/r0.1.0.html#association-lookup
|
||||
for details
|
||||
"""
|
||||
if not self._should_trust_id_server(id_server):
|
||||
raise SynapseError(
|
||||
400, "Untrusted ID server '%s'" % id_server, Codes.SERVER_NOT_TRUSTED
|
||||
def should_trust_id_server(hs, id_server):
|
||||
if id_server not in hs.config.trusted_third_party_id_servers:
|
||||
if hs.trust_any_id_server_just_for_testing_do_not_use:
|
||||
logger.warn(
|
||||
"Trusting untrustworthy ID server %r even though it isn't"
|
||||
" in the trusted id list for testing because"
|
||||
" 'use_insecure_ssl_client_just_for_testing_do_not_use'"
|
||||
" is set in the config",
|
||||
id_server,
|
||||
)
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
if not self._enable_lookup:
|
||||
raise AuthError(
|
||||
403, "Looking up third-party identifiers is denied from this server"
|
||||
)
|
||||
|
||||
target = self.rewrite_identity_server_urls.get(id_server, id_server)
|
||||
class LookupAlgorithm:
|
||||
"""
|
||||
Supported hashing algorithms when performing a 3PID lookup.
|
||||
|
||||
try:
|
||||
data = yield self.http_client.get_json(
|
||||
"https://%s/_matrix/identity/api/v1/lookup" % (target,),
|
||||
{"medium": medium, "address": address},
|
||||
)
|
||||
SHA256 - Hashing an (address, medium, pepper) combo with sha256, then url-safe base64
|
||||
encoding
|
||||
NONE - Not performing any hashing. Simply sending an (address, medium) combo in plaintext
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
except HttpResponseException as e:
|
||||
logger.info("Proxied lookup failed: %r", e)
|
||||
raise e.to_synapse_error()
|
||||
except IOError as e:
|
||||
logger.info("Failed to contact %r: %s", id_server, e)
|
||||
raise ProxiedRequestError(503, "Failed to contact identity server")
|
||||
|
||||
defer.returnValue(data)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def bulk_lookup_3pid(self, id_server, threepids):
|
||||
"""Looks up given 3pids in the passed identity server.
|
||||
|
||||
Args:
|
||||
id_server (str): The server name (including port, if required)
|
||||
of the identity server to use.
|
||||
threepids ([[str, str]]): The third party identifiers to lookup, as
|
||||
a list of 2-string sized lists ([medium, address]).
|
||||
|
||||
Returns:
|
||||
Deferred[dict]: The result of the lookup. See
|
||||
https://matrix.org/docs/spec/identity_service/r0.1.0.html#association-lookup
|
||||
for details
|
||||
"""
|
||||
if not self._should_trust_id_server(id_server):
|
||||
raise SynapseError(
|
||||
400, "Untrusted ID server '%s'" % id_server, Codes.SERVER_NOT_TRUSTED
|
||||
)
|
||||
|
||||
if not self._enable_lookup:
|
||||
raise AuthError(
|
||||
403, "Looking up third-party identifiers is denied from this server"
|
||||
)
|
||||
|
||||
target = self.rewrite_identity_server_urls.get(id_server, id_server)
|
||||
|
||||
try:
|
||||
data = yield self.http_client.post_json_get_json(
|
||||
"https://%s/_matrix/identity/api/v1/bulk_lookup" % (target,),
|
||||
{"threepids": threepids},
|
||||
)
|
||||
|
||||
except HttpResponseException as e:
|
||||
logger.info("Proxied lookup failed: %r", e)
|
||||
raise e.to_synapse_error()
|
||||
except IOError as e:
|
||||
logger.info("Failed to contact %r: %s", id_server, e)
|
||||
raise ProxiedRequestError(503, "Failed to contact identity server")
|
||||
|
||||
defer.returnValue(data)
|
||||
|
||||
@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():
|
||||
target = self.rewrite_identity_server_urls.get(
|
||||
server_hostname, server_hostname
|
||||
)
|
||||
|
||||
key_data = yield self.http_client.get_json(
|
||||
"https://%s/_matrix/identity/api/v1/pubkey/%s" % (target, 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
|
||||
|
||||
raise AuthError(401, "No signature from server %s" % (server_hostname,))
|
||||
SHA256 = "sha256"
|
||||
NONE = "none"
|
||||
|
||||
@@ -20,11 +20,14 @@ import logging
|
||||
|
||||
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 synapse import types
|
||||
from synapse.api.constants import EventTypes, Membership
|
||||
from synapse.api.ratelimiting import Ratelimiter
|
||||
from synapse.api.errors import (
|
||||
AuthError,
|
||||
Codes,
|
||||
@@ -32,9 +35,13 @@ from synapse.api.errors import (
|
||||
HttpResponseException,
|
||||
SynapseError,
|
||||
)
|
||||
from synapse.handlers.identity import LookupAlgorithm, should_trust_id_server
|
||||
from synapse.types import RoomID, UserID
|
||||
from synapse.util.async_helpers import Linearizer
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -78,7 +85,11 @@ class RoomMemberHandler(object):
|
||||
self.rewrite_identity_server_urls = self.config.rewrite_identity_server_urls
|
||||
self._enable_lookup = hs.config.enable_3pid_lookup
|
||||
self.allow_per_room_profiles = self.config.allow_per_room_profiles
|
||||
self.ratelimiter = Ratelimiter()
|
||||
|
||||
# This is only used to get at ratelimit function, and
|
||||
# maybe_kick_guest_users. It's fine there are multiple of these as
|
||||
# it doesn't store state.
|
||||
self.base_handler = BaseHandler(hs)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _remote_join(self, requester, remote_room_hosts, room_id, user, content):
|
||||
@@ -572,7 +583,7 @@ class RoomMemberHandler(object):
|
||||
event (SynapseEvent): The membership event.
|
||||
context: The context of the event.
|
||||
is_guest (bool): Whether the sender is a guest.
|
||||
room_hosts ([str]): Homeservers which are likely to already be in
|
||||
remote_room_hosts (list[str]|None): Homeservers which are likely to already be in
|
||||
the room, and could be danced with in order to join this
|
||||
homeserver for the first time.
|
||||
ratelimit (bool): Whether to rate limit this request.
|
||||
@@ -683,7 +694,7 @@ class RoomMemberHandler(object):
|
||||
servers.remove(room_alias.domain)
|
||||
servers.insert(0, room_alias.domain)
|
||||
|
||||
return (RoomID.from_string(room_id), servers)
|
||||
return RoomID.from_string(room_id), servers
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _get_inviter(self, user_id, room_id):
|
||||
@@ -714,7 +725,7 @@ class RoomMemberHandler(object):
|
||||
|
||||
# We need to rate limit *before* we send out any 3PID invites, so we
|
||||
# can't just rely on the standard ratelimiting of events.
|
||||
self.ratelimiter.ratelimit(
|
||||
self.base_handler.ratelimiter.ratelimit(
|
||||
requester.user.to_string(),
|
||||
time_now_s=self.hs.clock.time(),
|
||||
rate_hz=self.hs.config.rc_third_party_invite.per_second,
|
||||
@@ -742,7 +753,7 @@ class RoomMemberHandler(object):
|
||||
Codes.FORBIDDEN,
|
||||
)
|
||||
|
||||
invitee = yield self._lookup_3pid(id_server, medium, address)
|
||||
invitee = yield self.lookup_3pid(id_server, medium, address)
|
||||
|
||||
is_published = yield self.store.is_room_published(room_id)
|
||||
|
||||
@@ -766,22 +777,8 @@ class RoomMemberHandler(object):
|
||||
requester, id_server, medium, address, room_id, inviter, txn_id=txn_id
|
||||
)
|
||||
|
||||
def _get_id_server_target(self, id_server):
|
||||
"""Looks up an id_server's actual http endpoint
|
||||
|
||||
Args:
|
||||
id_server (str): the server name to lookup.
|
||||
|
||||
Returns:
|
||||
the http endpoint to connect to.
|
||||
"""
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
return self.rewrite_identity_server_urls[id_server]
|
||||
|
||||
return id_server
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _lookup_3pid(self, id_server, medium, address):
|
||||
def lookup_3pid(self, id_server, medium, address):
|
||||
"""Looks up a 3pid in the passed identity server.
|
||||
|
||||
Args:
|
||||
@@ -793,13 +790,216 @@ class RoomMemberHandler(object):
|
||||
Returns:
|
||||
str: the matrix ID of the 3pid, or None if it is not recognized.
|
||||
"""
|
||||
if not should_trust_id_server(self.hs, id_server):
|
||||
raise SynapseError(
|
||||
400, "Untrusted ID server '%s'" % id_server,
|
||||
Codes.SERVER_NOT_TRUSTED
|
||||
)
|
||||
|
||||
if not self._enable_lookup:
|
||||
raise SynapseError(
|
||||
403, "Looking up third-party identifiers is denied from this server"
|
||||
)
|
||||
|
||||
# Rewrite id_server URL if necessary
|
||||
id_server = self.rewrite_identity_server_urls.get(id_server, id_server)
|
||||
|
||||
# Check what hashing details are supported by this identity server
|
||||
use_v1 = False
|
||||
hash_details = None
|
||||
try:
|
||||
data = yield self.identity_handler.lookup_3pid(id_server, medium, address)
|
||||
return data.get("mxid")
|
||||
hash_details = yield self.simple_http_client.get_json(
|
||||
"%s%s/_matrix/identity/v2/hash_details" % (id_server_scheme, id_server)
|
||||
)
|
||||
except (HttpResponseException, ValueError) as e:
|
||||
# Catch HttpResponseExcept for a non-200 response code
|
||||
# Catch ValueError for non-JSON response body
|
||||
|
||||
# Check if this identity server does not know about v2 lookups
|
||||
if e.code == 404:
|
||||
# This is an old identity server that does not yet support v2 lookups
|
||||
use_v1 = True
|
||||
else:
|
||||
logger.warn("Error when looking up hashing details: %s" % (e,))
|
||||
return None
|
||||
|
||||
if use_v1:
|
||||
return (yield self._lookup_3pid_v1(id_server, medium, address))
|
||||
|
||||
return (yield self._lookup_3pid_v2(id_server, medium, address, hash_details))
|
||||
|
||||
@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 ProxiedRequestError as e:
|
||||
logger.warn("Error from identity server lookup: %s" % (e,))
|
||||
|
||||
except IOError as e:
|
||||
logger.warn("Error from identity server lookup: %s" % (e,))
|
||||
|
||||
return None
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _lookup_3pid_v2(self, id_server, medium, address, hash_details):
|
||||
"""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.
|
||||
medium (str): The type of the third party identifier (e.g. "email").
|
||||
address (str): The third party identifier (e.g. "foo@example.com").
|
||||
hash_details (dict[str, str|list]): A dictionary containing hashing information
|
||||
provided by an identity server.
|
||||
|
||||
Returns:
|
||||
Deferred[str|None]: the matrix ID of the 3pid, or None if it is not recognised.
|
||||
"""
|
||||
# Extract information from hash_details
|
||||
supported_lookup_algorithms = hash_details["algorithms"]
|
||||
lookup_pepper = hash_details["lookup_pepper"]
|
||||
|
||||
# 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.warn(
|
||||
"None of the provided lookup algorithms of %s%s are supported: %s",
|
||||
id_server_scheme,
|
||||
id_server,
|
||||
hash_details["algorithms"],
|
||||
)
|
||||
raise SynapseError(
|
||||
400,
|
||||
"Provided identity server does not support any v2 lookup "
|
||||
"algorithms that this homeserver supports.",
|
||||
)
|
||||
|
||||
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,
|
||||
},
|
||||
)
|
||||
except (HttpResponseException, ValueError) as e:
|
||||
# Catch HttpResponseExcept for a non-200 response code
|
||||
# Catch ValueError for non-JSON response body
|
||||
logger.warn("Error when performing a 3pid lookup: %s" % (e,))
|
||||
return None
|
||||
|
||||
# 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.debug("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 bulk_lookup_3pid(self, id_server, threepids):
|
||||
"""Looks up given 3pids in the passed identity server.
|
||||
Args:
|
||||
id_server (str): The server name (including port, if required)
|
||||
of the identity server to use.
|
||||
threepids ([[str, str]]): The third party identifiers to lookup, as
|
||||
a list of 2-string sized lists ([medium, address]).
|
||||
Returns:
|
||||
Deferred[dict]: The result of the lookup. See
|
||||
https://matrix.org/docs/spec/identity_service/r0.1.0.html#association-lookup
|
||||
for details
|
||||
"""
|
||||
if not should_trust_id_server(self.hs, id_server):
|
||||
raise SynapseError(
|
||||
400, "Untrusted ID server '%s'" % id_server,
|
||||
Codes.SERVER_NOT_TRUSTED
|
||||
)
|
||||
|
||||
if not self._enable_lookup:
|
||||
raise AuthError(
|
||||
403, "Looking up third-party identifiers is denied from this server",
|
||||
)
|
||||
|
||||
target = self.rewrite_identity_server_urls.get(id_server, id_server)
|
||||
|
||||
try:
|
||||
data = yield self.simple_http_client.post_json_get_json(
|
||||
"https://%s/_matrix/identity/api/v1/bulk_lookup" % (target,),
|
||||
{
|
||||
"threepids": threepids,
|
||||
}
|
||||
)
|
||||
|
||||
except HttpResponseException as e:
|
||||
logger.info("Proxied lookup failed: %r", e)
|
||||
raise e.to_synapse_error()
|
||||
except IOError as e:
|
||||
logger.info("Failed to contact %r: %s", id_server, e)
|
||||
raise ProxiedRequestError(503, "Failed to contact identity server")
|
||||
|
||||
defer.returnValue(data)
|
||||
|
||||
@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.simple_http_client.get_json(
|
||||
"%s%s/_matrix/identity/api/v1/pubkey/%s"
|
||||
% (id_server_scheme, 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, requester, id_server, medium, address, room_id, user, txn_id
|
||||
@@ -917,11 +1117,10 @@ class RoomMemberHandler(object):
|
||||
display_name (str): A user-friendly name to represent the invited
|
||||
user.
|
||||
"""
|
||||
|
||||
target = self._get_id_server_target(id_server)
|
||||
id_server = self.rewrite_identity_server_urls.get(id_server, id_server)
|
||||
is_url = "%s%s/_matrix/identity/api/v1/store-invite" % (
|
||||
id_server_scheme,
|
||||
target,
|
||||
id_server,
|
||||
)
|
||||
|
||||
invite_config = {
|
||||
@@ -961,7 +1160,7 @@ class RoomMemberHandler(object):
|
||||
fallback_public_key = {
|
||||
"public_key": data["public_key"],
|
||||
"key_validity_url": "%s%s/_matrix/identity/api/v1/pubkey/isvalid"
|
||||
% (id_server_scheme, target),
|
||||
% (id_server_scheme, id_server),
|
||||
}
|
||||
else:
|
||||
fallback_public_key = public_keys[0]
|
||||
@@ -1028,9 +1227,7 @@ class RoomMemberMasterHandler(RoomMemberHandler):
|
||||
)
|
||||
|
||||
if complexity:
|
||||
if complexity["v1"] > max_complexity:
|
||||
return True
|
||||
return False
|
||||
return complexity["v1"] > max_complexity
|
||||
return None
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@@ -1046,10 +1243,7 @@ class RoomMemberMasterHandler(RoomMemberHandler):
|
||||
max_complexity = self.hs.config.limit_remote_rooms.complexity
|
||||
complexity = yield self.store.get_room_complexity(room_id)
|
||||
|
||||
if complexity["v1"] > max_complexity:
|
||||
return True
|
||||
|
||||
return False
|
||||
return complexity["v1"] > max_complexity
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _remote_join(self, requester, remote_room_hosts, room_id, user, content):
|
||||
|
||||
@@ -706,6 +706,7 @@ class ThreepidLookupRestServlet(RestServlet):
|
||||
super(ThreepidLookupRestServlet, self).__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self.identity_handler = hs.get_handlers().identity_handler
|
||||
self.room_member_handler = hs.get_room_member_handler()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request):
|
||||
@@ -725,7 +726,7 @@ class ThreepidLookupRestServlet(RestServlet):
|
||||
|
||||
# Proxy the request to the identity server. lookup_3pid handles checking
|
||||
# if the lookup is allowed so we don't need to do it here.
|
||||
ret = yield self.identity_handler.lookup_3pid(id_server, medium, address)
|
||||
ret = yield self.room_member_handler.lookup_3pid(id_server, medium, address)
|
||||
|
||||
defer.returnValue((200, ret))
|
||||
|
||||
@@ -737,6 +738,7 @@ class ThreepidBulkLookupRestServlet(RestServlet):
|
||||
super(ThreepidBulkLookupRestServlet, self).__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self.identity_handler = hs.get_handlers().identity_handler
|
||||
self.room_member_handler = hs.get_room_member_handler()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
@@ -751,7 +753,7 @@ class ThreepidBulkLookupRestServlet(RestServlet):
|
||||
|
||||
# Proxy the request to the identity server. lookup_3pid handles checking
|
||||
# if the lookup is allowed so we don't need to do it here.
|
||||
ret = yield self.identity_handler.bulk_lookup_3pid(
|
||||
ret = yield self.room_member_handler.bulk_lookup_3pid(
|
||||
body["id_server"], body["threepids"]
|
||||
)
|
||||
|
||||
|
||||
33
synapse/util/hash.py
Normal file
33
synapse/util/hash.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import hashlib
|
||||
|
||||
import unpaddedbase64
|
||||
|
||||
|
||||
def sha256_and_url_safe_base64(input_text):
|
||||
"""SHA256 hash an input string, encode the digest as url-safe base64, and
|
||||
return
|
||||
|
||||
:param input_text: string to hash
|
||||
:type input_text: str
|
||||
|
||||
:returns a sha256 hashed and url-safe base64 encoded digest
|
||||
:rtype: str
|
||||
"""
|
||||
digest = hashlib.sha256(input_text.encode()).digest()
|
||||
return unpaddedbase64.encode_base64(digest, urlsafe=True)
|
||||
@@ -20,6 +20,7 @@ from mock import Mock
|
||||
from twisted.internet import defer
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.api.errors import HttpResponseException
|
||||
from synapse.rest.client.v1 import login, room
|
||||
from synapse.rest.client.v2_alpha import account
|
||||
|
||||
@@ -109,8 +110,15 @@ class IdentityEnabledTestCase(unittest.HomeserverTestCase):
|
||||
config["enable_3pid_lookup"] = True
|
||||
config["trusted_third_party_id_servers"] = ["testis"]
|
||||
|
||||
def get_json(uri, args={}, headers=None):
|
||||
# TODO: Mock v2 hash_details endpoints and don't just run the v1 code
|
||||
if "/hash_details" in uri:
|
||||
raise HttpResponseException(404, "I am a happy v1 server", b"{}")
|
||||
|
||||
return defer.succeed((200, "{}"))
|
||||
|
||||
mock_http_client = Mock(spec=["get_json", "post_json_get_json"])
|
||||
mock_http_client.get_json.return_value = defer.succeed((200, "{}"))
|
||||
mock_http_client.get_json.side_effect = get_json
|
||||
mock_http_client.post_json_get_json.return_value = defer.succeed((200, "{}"))
|
||||
|
||||
self.hs = self.setup_test_homeserver(
|
||||
@@ -143,8 +151,10 @@ class IdentityEnabledTestCase(unittest.HomeserverTestCase):
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
# There will be a call to hash_details, which will 404
|
||||
# then /lookup. Thus we don't use `assert_called_with_once` here
|
||||
get_json = self.hs.get_simple_http_client().get_json
|
||||
get_json.assert_called_once_with(
|
||||
get_json.assert_called_with(
|
||||
"https://testis/_matrix/identity/api/v1/lookup",
|
||||
{"address": "test@example.com", "medium": "email"},
|
||||
)
|
||||
@@ -157,8 +167,10 @@ class IdentityEnabledTestCase(unittest.HomeserverTestCase):
|
||||
request, channel = self.make_request("GET", url, access_token=self.tok)
|
||||
self.render(request)
|
||||
|
||||
# There will be a call to hash_details, which will 404
|
||||
# then /lookup. Thus we don't use `assert_called_with_once` here
|
||||
get_json = self.hs.get_simple_http_client().get_json
|
||||
get_json.assert_called_once_with(
|
||||
get_json.assert_called_with(
|
||||
"https://testis/_matrix/identity/api/v1/lookup",
|
||||
{"address": "foo@bar.baz", "medium": "email"},
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ from mock import Mock
|
||||
|
||||
from twisted.internet import defer
|
||||
from synapse.api.constants import EventTypes, JoinRules, RoomCreationPreset
|
||||
from synapse.api.errors import HttpResponseException
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client.v1 import login, room
|
||||
from synapse.third_party_rules.access_rules import (
|
||||
@@ -58,6 +59,10 @@ class RoomAccessTestCase(unittest.HomeserverTestCase):
|
||||
return defer.succeed(pdu)
|
||||
|
||||
def get_json(uri, args={}, headers=None):
|
||||
# TODO: Mock v2 hash_details endpoints and don't just run the v1 code
|
||||
if "/hash_details" in uri:
|
||||
raise HttpResponseException(404, "I am a happy v1 server", b"{}")
|
||||
|
||||
address_domain = args["address"].split("@")[1]
|
||||
return defer.succeed({"hs": address_domain})
|
||||
|
||||
@@ -332,6 +337,7 @@ class RoomAccessTestCase(unittest.HomeserverTestCase):
|
||||
self.hs.config.rc_third_party_invite.burst_count = 10
|
||||
self.hs.config.rc_third_party_invite.per_second = 0.1
|
||||
|
||||
# Note: These calls trigger the mocked get_json method above
|
||||
# We can't send a 3PID invite to a room that already has two members.
|
||||
self.send_threepid_invite(
|
||||
address="test@allowed_domain",
|
||||
|
||||
@@ -52,7 +52,6 @@ class RegisterRestServletTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
return self.hs
|
||||
|
||||
@unittest.DEBUG
|
||||
def test_POST_appservice_registration_valid(self):
|
||||
user_id = "@as_user_kermit:test"
|
||||
as_token = "i_am_an_app_service"
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
|
||||
import json
|
||||
from mock import Mock
|
||||
from twisted.internet import defer
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.config._base import ConfigError
|
||||
@@ -287,6 +289,9 @@ class DomainRuleCheckerRoomTestCase(unittest.HomeserverTestCase):
|
||||
def test_cannot_3pid_invite(self):
|
||||
"""Test that unbound 3pid invites get rejected.
|
||||
"""
|
||||
self.hs.get_room_member_handler().lookup_3pid = Mock()
|
||||
self.hs.get_room_member_handler().lookup_3pid.return_value = defer.succeed(None)
|
||||
|
||||
channel = self._create_room(self.admin_access_token)
|
||||
assert channel.result["code"] == b"200", channel.result
|
||||
|
||||
|
||||
Reference in New Issue
Block a user