bind.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014 OpenMarket Ltd
  3. # Copyright 2018 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 twisted.internet import defer
  30. logger = logging.getLogger(__name__)
  31. class ThreepidBinder:
  32. # the lifetime of a 3pid association
  33. THREEPID_ASSOCIATION_LIFETIME_MS = 100 * 365 * 24 * 60 * 60 * 1000
  34. def __init__(self, sydent):
  35. self.sydent = sydent
  36. self.hashing_store = HashingMetadataStore(sydent)
  37. def addBinding(self, medium, address, mxid):
  38. """
  39. Binds the given 3pid to the given mxid.
  40. It's assumed that we have somehow validated that the given user owns
  41. the given 3pid
  42. :param medium: The medium of the 3PID to bind.
  43. :type medium: unicode
  44. :param address: The address of the 3PID to bind.
  45. :type address: unicode
  46. :param mxid: The MXID to bind the 3PID to.
  47. :type mxid: unicode
  48. :return: The signed association.
  49. :rtype: dict[str, any]
  50. """
  51. localAssocStore = LocalAssociationStore(self.sydent)
  52. # Fill out the association details
  53. createdAt = time_msec()
  54. expires = createdAt + ThreepidBinder.THREEPID_ASSOCIATION_LIFETIME_MS
  55. # Hash the medium + address and store that hash for the purposes of
  56. # later lookups
  57. str_to_hash = u' '.join(
  58. [address, medium, self.hashing_store.get_lookup_pepper()],
  59. )
  60. lookup_hash = sha256_and_url_safe_base64(str_to_hash)
  61. assoc = ThreepidAssociation(
  62. medium, address, lookup_hash, mxid, createdAt, createdAt, expires,
  63. )
  64. localAssocStore.addOrUpdateAssociation(assoc)
  65. self.sydent.pusher.doLocalPush()
  66. joinTokenStore = JoinTokenStore(self.sydent)
  67. pendingJoinTokens = joinTokenStore.getTokens(medium, address)
  68. invites = []
  69. for token in pendingJoinTokens:
  70. token["mxid"] = mxid
  71. token["signed"] = {
  72. "mxid": mxid,
  73. "token": token["token"],
  74. }
  75. token["signed"] = signedjson.sign.sign_json(token["signed"], self.sydent.server_name, self.sydent.keyring.ed25519)
  76. invites.append(token)
  77. if invites:
  78. assoc.extra_fields["invites"] = invites
  79. joinTokenStore.markTokensAsSent(medium, address)
  80. signer = Signer(self.sydent)
  81. sgassoc = signer.signedThreePidAssociation(assoc)
  82. self._notify(sgassoc, 0)
  83. return sgassoc
  84. def removeBinding(self, threepid, mxid):
  85. """
  86. Removes the binding between a given 3PID and a given MXID.
  87. :param threepid: The 3PID of the binding to remove.
  88. :type threepid: dict[unicode, unicode]
  89. :param mxid: The MXID of the binding to remove.
  90. :type mxid: unicode
  91. """
  92. localAssocStore = LocalAssociationStore(self.sydent)
  93. localAssocStore.removeAssociation(threepid, mxid)
  94. self.sydent.pusher.doLocalPush()
  95. @defer.inlineCallbacks
  96. def _notify(self, assoc, attempt):
  97. """
  98. Sends data about a new association (and, if necessary, the associated invites)
  99. to the associated MXID's homeserver.
  100. :param assoc: The association to send down to the homeserver.
  101. :type assoc: dict[str, any]
  102. :param attempt: The number of previous attempts to send this association.
  103. :type attempt: int
  104. """
  105. mxid = assoc["mxid"]
  106. mxid_parts = mxid.split(":", 1)
  107. if len(mxid_parts) != 2:
  108. logger.error(
  109. "Can't notify on bind for unparseable mxid %s. Not retrying.",
  110. assoc["mxid"],
  111. )
  112. return
  113. post_url = "matrix://%s/_matrix/federation/v1/3pid/onbind" % (
  114. mxid_parts[1],
  115. )
  116. logger.info("Making bind callback to: %s", post_url)
  117. # Make a POST to the chosen Synapse server
  118. http_client = FederationHttpClient(self.sydent)
  119. try:
  120. response = yield http_client.post_json_get_nothing(post_url, assoc, {})
  121. except Exception as e:
  122. self._notifyErrback(assoc, attempt, e)
  123. return
  124. # If the request failed, try again with exponential backoff
  125. if response.code != 200:
  126. self._notifyErrback(
  127. assoc, attempt, "Non-OK error code received (%d)" % response.code
  128. )
  129. else:
  130. logger.info("Successfully notified on bind for %s" % (mxid,))
  131. # Skip the deletion step if instructed so by the config.
  132. if not self.sydent.delete_tokens_on_bind:
  133. return
  134. # Only remove sent tokens when they've been successfully sent.
  135. try:
  136. joinTokenStore = JoinTokenStore(self.sydent)
  137. joinTokenStore.deleteTokens(assoc["medium"], assoc["address"])
  138. logger.info(
  139. "Successfully deleted invite for %s from the store",
  140. assoc["address"],
  141. )
  142. except Exception as e:
  143. logger.exception(
  144. "Couldn't remove invite for %s from the store",
  145. assoc["address"],
  146. )
  147. def _notifyErrback(self, assoc, attempt, error):
  148. """
  149. Handles errors when trying to send an association down to a homeserver by
  150. logging the error and scheduling a new attempt.
  151. :param assoc: The association to send down to the homeserver.
  152. :type assoc: dict[str, any]
  153. :param attempt: The number of previous attempts to send this association.
  154. :type attempt: int
  155. :param error: The error that was raised when trying to send the association.
  156. :type error: Exception
  157. """
  158. logger.warning(
  159. "Error notifying on bind for %s: %s - rescheduling", assoc["mxid"], error
  160. )
  161. self.sydent.reactor.callLater(
  162. math.pow(2, attempt), self._notify, assoc, attempt + 1
  163. )
  164. # The below is lovingly ripped off of synapse/http/endpoint.py
  165. _Server = collections.namedtuple("_Server", "priority weight host port")