bind.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014 OpenMarket Ltd
  3. # Copyright 2018, 2019 New Vector Ltd
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. from __future__ import absolute_import
  17. import collections
  18. import logging
  19. import math
  20. import signedjson.sign
  21. from sydent.db.invite_tokens import JoinTokenStore
  22. from sydent.db.threepid_associations import LocalAssociationStore
  23. from sydent.util import time_msec
  24. from sydent.util.hash import sha256_and_url_safe_base64
  25. from sydent.db.hashing_metadata import HashingMetadataStore
  26. from sydent.threepid.signer import Signer
  27. from sydent.http.httpclient import FederationHttpClient
  28. from sydent.threepid import ThreepidAssociation
  29. from sydent.util.stringutils import is_valid_matrix_server_name
  30. from twisted.internet import defer
  31. logger = logging.getLogger(__name__)
  32. def parseMxid(mxid):
  33. if len(mxid) > 255:
  34. raise Exception("This mxid is too long")
  35. if len(mxid) == 0 or mxid[0:1] != "@":
  36. raise Exception("mxid does not start with '@'")
  37. parts = mxid[1:].split(':', 1)
  38. if len(parts) != 2:
  39. raise Exception("Not enough colons in mxid")
  40. return parts
  41. class BindingNotPermittedException(Exception):
  42. pass
  43. class ThreepidBinder:
  44. # the lifetime of a 3pid association
  45. THREEPID_ASSOCIATION_LIFETIME_MS = 100 * 365 * 24 * 60 * 60 * 1000
  46. def __init__(self, sydent, info):
  47. self.sydent = sydent
  48. self._info = info
  49. self.hashing_store = HashingMetadataStore(sydent)
  50. def addBinding(self, medium, address, mxid, check_info=True):
  51. """
  52. Binds the given 3pid to the given mxid.
  53. It's assumed that we have somehow validated that the given user owns
  54. the given 3pid
  55. :param medium: The medium of the 3PID to bind.
  56. :type medium: unicode
  57. :param address: The address of the 3PID to bind.
  58. :type address: unicode
  59. :param mxid: The MXID to bind the 3PID to.
  60. :type mxid: unicode
  61. :param check_info: Whether to check the address against the info file. Setting
  62. this to False should only be done when testing.
  63. :type check_info: bool
  64. :return: The signed association.
  65. :rtype: dict[str, any]
  66. """
  67. mxidParts = parseMxid(mxid)
  68. if check_info:
  69. result = self._info.match_user_id(medium, address)
  70. possible_hses = []
  71. if 'hs' in result:
  72. possible_hses.append(result['hs'])
  73. if 'shadow_hs' in result:
  74. possible_hses.append(result['shadow_hs'])
  75. if mxidParts[1] not in possible_hses:
  76. logger.info("Denying bind of %r/%r -> %r (info result: %r)", medium, address, mxid, result)
  77. raise BindingNotPermittedException()
  78. localAssocStore = LocalAssociationStore(self.sydent)
  79. # Fill out the association details
  80. createdAt = time_msec()
  81. expires = createdAt + ThreepidBinder.THREEPID_ASSOCIATION_LIFETIME_MS
  82. # Hash the medium + address and store that hash for the purposes of
  83. # later lookups
  84. str_to_hash = u' '.join(
  85. [address, medium, self.hashing_store.get_lookup_pepper()],
  86. )
  87. lookup_hash = sha256_and_url_safe_base64(str_to_hash)
  88. assoc = ThreepidAssociation(
  89. medium, address, lookup_hash, mxid, createdAt, createdAt, expires,
  90. )
  91. localAssocStore.addOrUpdateAssociation(assoc)
  92. self.sydent.pusher.doLocalPush()
  93. signer = Signer(self.sydent)
  94. sgassoc = signer.signedThreePidAssociation(assoc)
  95. return sgassoc
  96. def notifyPendingInvites(self, assoc):
  97. # this is called back by the replication code once we see new bindings
  98. # (including local ones created by addBinding() above)
  99. joinTokenStore = JoinTokenStore(self.sydent)
  100. pendingJoinTokens = joinTokenStore.getTokens(assoc.medium, assoc.address)
  101. invites = []
  102. for token in pendingJoinTokens:
  103. # only notify for join tokens we created ourselves,
  104. # not replicated ones: the HS can only claim the 3pid
  105. # invite if it has a signature from the IS whose public
  106. # key is in the 3pid invite event. This will only be us
  107. # if we created the invite, not if the invite was replicated
  108. # to us.
  109. if token['origin_server'] is None:
  110. token["mxid"] = assoc.mxid
  111. token["signed"] = {
  112. "mxid": assoc.mxid,
  113. "token": token["token"],
  114. }
  115. token["signed"] = signedjson.sign.sign_json(token["signed"], self.sydent.server_name, self.sydent.keyring.ed25519)
  116. invites.append(token)
  117. if len(invites) > 0:
  118. assoc.extra_fields["invites"] = invites
  119. joinTokenStore.markTokensAsSent(assoc.medium, assoc.address)
  120. signer = Signer(self.sydent)
  121. sgassoc = signer.signedThreePidAssociation(assoc)
  122. self._notify(sgassoc, 0)
  123. return sgassoc
  124. return None
  125. def removeBinding(self, threepid, mxid):
  126. """
  127. Removes the binding between a given 3PID and a given MXID.
  128. :param threepid: The 3PID of the binding to remove.
  129. :type threepid: dict[unicode, unicode]
  130. :param mxid: The MXID of the binding to remove.
  131. :type mxid: unicode
  132. """
  133. localAssocStore = LocalAssociationStore(self.sydent)
  134. localAssocStore.removeAssociation(threepid, mxid)
  135. self.sydent.pusher.doLocalPush()
  136. @defer.inlineCallbacks
  137. def _notify(self, assoc, attempt):
  138. """
  139. Sends data about a new association (and, if necessary, the associated invites)
  140. to the associated MXID's homeserver.
  141. :param assoc: The association to send down to the homeserver.
  142. :type assoc: dict[str, any]
  143. :param attempt: The number of previous attempts to send this association.
  144. :type attempt: int
  145. """
  146. mxid = assoc["mxid"]
  147. mxid_parts = mxid.split(":", 1)
  148. if len(mxid_parts) != 2:
  149. logger.error(
  150. "Can't notify on bind for unparseable mxid %s. Not retrying.",
  151. assoc["mxid"],
  152. )
  153. return
  154. matrix_server = mxid_parts[1]
  155. if not is_valid_matrix_server_name(matrix_server):
  156. logger.error(
  157. "MXID server part '%s' not a valid Matrix server name. Not retrying.",
  158. matrix_server,
  159. )
  160. return
  161. post_url = "matrix://%s/_matrix/federation/v1/3pid/onbind" % (
  162. matrix_server,
  163. )
  164. logger.info("Making bind callback to: %s", post_url)
  165. # Make a POST to the chosen Synapse server
  166. http_client = FederationHttpClient(self.sydent)
  167. try:
  168. response = yield http_client.post_json_get_nothing(post_url, assoc, {})
  169. except Exception as e:
  170. self._notifyErrback(assoc, attempt, e)
  171. return
  172. if response.code == 403:
  173. # If the request failed with a 403, then the token is not recognised
  174. logger.warning(
  175. "Attempted to notify on bind for %s but received 403. Not retrying."
  176. % (mxid,)
  177. )
  178. return
  179. elif response.code != 200:
  180. # Otherwise, try again with exponential backoff
  181. self._notifyErrback(
  182. assoc, attempt, "Non-OK error code received (%d)" % response.code
  183. )
  184. return
  185. logger.info("Successfully notified on bind for %s" % (mxid,))
  186. # Skip the deletion step if instructed so by the config.
  187. if not self.sydent.delete_tokens_on_bind:
  188. return
  189. # Only remove sent tokens when they've been successfully sent.
  190. try:
  191. joinTokenStore = JoinTokenStore(self.sydent)
  192. joinTokenStore.deleteTokens(assoc["medium"], assoc["address"])
  193. logger.info(
  194. "Successfully deleted invite for %s from the store",
  195. assoc["address"],
  196. )
  197. except Exception:
  198. logger.exception(
  199. "Couldn't remove invite for %s from the store",
  200. assoc["address"],
  201. )
  202. def _notifyErrback(self, assoc, attempt, error):
  203. """
  204. Handles errors when trying to send an association down to a homeserver by
  205. logging the error and scheduling a new attempt.
  206. :param assoc: The association to send down to the homeserver.
  207. :type assoc: dict[str, any]
  208. :param attempt: The number of previous attempts to send this association.
  209. :type attempt: int
  210. :param error: The error that was raised when trying to send the association.
  211. :type error: Exception
  212. """
  213. logger.warning(
  214. "Error notifying on bind for %s: %s - rescheduling", assoc["mxid"], error
  215. )
  216. self.sydent.reactor.callLater(
  217. math.pow(2, attempt), self._notify, assoc, attempt + 1
  218. )
  219. # The below is lovingly ripped off of synapse/http/endpoint.py
  220. _Server = collections.namedtuple("_Server", "priority weight host port")