bind.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. # Copyright 2014 OpenMarket Ltd
  2. # Copyright 2018 New Vector Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import collections
  16. import logging
  17. import math
  18. from typing import TYPE_CHECKING, Any, Dict, Union, cast
  19. import signedjson.sign # type: ignore
  20. from twisted.internet import defer
  21. from sydent.db.hashing_metadata import HashingMetadataStore
  22. from sydent.db.invite_tokens import JoinTokenStore
  23. from sydent.db.threepid_associations import LocalAssociationStore
  24. from sydent.http.httpclient import FederationHttpClient
  25. from sydent.threepid import ThreepidAssociation
  26. from sydent.threepid.signer import Signer
  27. from sydent.util import time_msec
  28. from sydent.util.hash import sha256_and_url_safe_base64
  29. from sydent.util.stringutils import is_valid_matrix_server_name, normalise_address
  30. if TYPE_CHECKING:
  31. from sydent.sydent import Sydent
  32. logger = logging.getLogger(__name__)
  33. class ThreepidBinder:
  34. # the lifetime of a 3pid association
  35. THREEPID_ASSOCIATION_LIFETIME_MS = 100 * 365 * 24 * 60 * 60 * 1000
  36. def __init__(self, sydent: "Sydent") -> None:
  37. self.sydent = sydent
  38. self.hashing_store = HashingMetadataStore(sydent)
  39. def addBinding(self, medium: str, address: str, mxid: str) -> Dict[str, Any]:
  40. """
  41. Binds the given 3pid to the given mxid.
  42. It's assumed that we have somehow validated that the given user owns
  43. the given 3pid
  44. :param medium: The medium of the 3PID to bind.
  45. :param address: The address of the 3PID to bind.
  46. :param mxid: The MXID to bind the 3PID to.
  47. :return: The signed association.
  48. """
  49. # ensure we casefold email address before storing
  50. normalised_address = normalise_address(address, medium)
  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 = " ".join(
  58. [normalised_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,
  63. normalised_address,
  64. lookup_hash,
  65. mxid,
  66. createdAt,
  67. createdAt,
  68. expires,
  69. )
  70. localAssocStore.addOrUpdateAssociation(assoc)
  71. self.sydent.pusher.doLocalPush()
  72. joinTokenStore = JoinTokenStore(self.sydent)
  73. pendingJoinTokens = joinTokenStore.getTokens(medium, normalised_address)
  74. invites = []
  75. for token in pendingJoinTokens:
  76. token["mxid"] = mxid
  77. token["signed"] = {
  78. "mxid": mxid,
  79. "token": cast(str, token["token"]),
  80. }
  81. token["signed"] = signedjson.sign.sign_json(
  82. token["signed"],
  83. self.sydent.config.general.server_name,
  84. self.sydent.keyring.ed25519,
  85. )
  86. invites.append(token)
  87. if invites:
  88. assoc.extra_fields["invites"] = invites
  89. joinTokenStore.markTokensAsSent(medium, normalised_address)
  90. signer = Signer(self.sydent)
  91. sgassoc = signer.signedThreePidAssociation(assoc)
  92. defer.ensureDeferred(self._notify(sgassoc, 0))
  93. return sgassoc
  94. def removeBinding(self, threepid: Dict[str, str], mxid: str) -> None:
  95. """
  96. Removes the binding between a given 3PID and a given MXID.
  97. :param threepid: The 3PID of the binding to remove.
  98. :param mxid: The MXID of the binding to remove.
  99. """
  100. # ensure we are casefolding email addresses
  101. threepid["address"] = normalise_address(threepid["address"], threepid["email"])
  102. localAssocStore = LocalAssociationStore(self.sydent)
  103. localAssocStore.removeAssociation(threepid, mxid)
  104. self.sydent.pusher.doLocalPush()
  105. async def _notify(self, assoc: Dict[str, Any], attempt: int) -> None:
  106. """
  107. Sends data about a new association (and, if necessary, the associated invites)
  108. to the associated MXID's homeserver.
  109. :param assoc: The association to send down to the homeserver.
  110. :param attempt: The number of previous attempts to send this association.
  111. """
  112. mxid = assoc["mxid"]
  113. mxid_parts = mxid.split(":", 1)
  114. if len(mxid_parts) != 2:
  115. logger.error(
  116. "Can't notify on bind for unparseable mxid %s. Not retrying.",
  117. assoc["mxid"],
  118. )
  119. return
  120. matrix_server = mxid_parts[1]
  121. if not is_valid_matrix_server_name(matrix_server):
  122. logger.error(
  123. "MXID server part '%s' not a valid Matrix server name. Not retrying.",
  124. matrix_server,
  125. )
  126. return
  127. post_url = "matrix://%s/_matrix/federation/v1/3pid/onbind" % (matrix_server,)
  128. logger.info("Making bind callback to: %s", post_url)
  129. # Make a POST to the chosen Synapse server
  130. http_client = FederationHttpClient(self.sydent)
  131. try:
  132. response = await http_client.post_json_get_nothing(post_url, assoc, {})
  133. except Exception as e:
  134. self._notifyErrback(assoc, attempt, e)
  135. return
  136. # If the request failed, try again with exponential backoff
  137. if response.code != 200:
  138. self._notifyErrback(
  139. assoc, attempt, "Non-OK error code received (%d)" % response.code
  140. )
  141. else:
  142. logger.info("Successfully notified on bind for %s" % (mxid,))
  143. # Skip the deletion step if instructed so by the config.
  144. if not self.sydent.delete_tokens_on_bind:
  145. return
  146. # Only remove sent tokens when they've been successfully sent.
  147. try:
  148. joinTokenStore = JoinTokenStore(self.sydent)
  149. joinTokenStore.deleteTokens(assoc["medium"], assoc["address"])
  150. logger.info(
  151. "Successfully deleted invite for %s from the store",
  152. assoc["address"],
  153. )
  154. except Exception:
  155. logger.exception(
  156. "Couldn't remove invite for %s from the store",
  157. assoc["address"],
  158. )
  159. def _notifyErrback(
  160. self, assoc: Dict[str, Any], attempt: int, error: Union[Exception, str]
  161. ) -> None:
  162. """
  163. Handles errors when trying to send an association down to a homeserver by
  164. logging the error and scheduling a new attempt.
  165. :param assoc: The association to send down to the homeserver.
  166. :param attempt: The number of previous attempts to send this association.
  167. :param error: The error that was raised when trying to send the association.
  168. """
  169. logger.warning(
  170. "Error notifying on bind for %s: %s - rescheduling", assoc["mxid"], error
  171. )
  172. self.sydent.reactor.callLater(
  173. math.pow(2, attempt), self._notify, assoc, attempt + 1
  174. )
  175. # The below is lovingly ripped off of synapse/http/endpoint.py
  176. _Server = collections.namedtuple("_Server", "priority weight host port")