store_invite_servlet.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2015 OpenMarket 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. from __future__ import absolute_import
  16. import nacl.signing
  17. import random
  18. import string
  19. from email.header import Header
  20. from six import string_types
  21. from twisted.web.resource import Resource
  22. from unpaddedbase64 import encode_base64
  23. from sydent.db.invite_tokens import JoinTokenStore
  24. from sydent.db.threepid_associations import GlobalAssociationStore
  25. from sydent.http.servlets import get_args, send_cors, jsonwrap
  26. from sydent.http.auth import authIfV2
  27. from sydent.util.emailutils import sendEmail
  28. class StoreInviteServlet(Resource):
  29. def __init__(self, syd):
  30. self.sydent = syd
  31. self.random = random.SystemRandom()
  32. @jsonwrap
  33. def render_POST(self, request):
  34. send_cors(request)
  35. authIfV2(self.sydent, request)
  36. args = get_args(request, ("medium", "address", "room_id", "sender",))
  37. medium = args["medium"]
  38. address = args["address"]
  39. roomId = args["room_id"]
  40. sender = args["sender"]
  41. globalAssocStore = GlobalAssociationStore(self.sydent)
  42. mxid = globalAssocStore.getMxid(medium, address)
  43. if mxid:
  44. request.setResponseCode(400)
  45. return {
  46. "errcode": "M_THREEPID_IN_USE",
  47. "error": "Binding already known",
  48. "mxid": mxid,
  49. }
  50. if medium != "email":
  51. request.setResponseCode(400)
  52. return {
  53. "errcode": "M_UNRECOGNIZED",
  54. "error": "Didn't understand medium '%s'" % (medium,),
  55. }
  56. token = self._randomString(128)
  57. tokenStore = JoinTokenStore(self.sydent)
  58. ephemeralPrivateKey = nacl.signing.SigningKey.generate()
  59. ephemeralPublicKey = ephemeralPrivateKey.verify_key
  60. ephemeralPrivateKeyBase64 = encode_base64(ephemeralPrivateKey.encode(), True)
  61. ephemeralPublicKeyBase64 = encode_base64(ephemeralPublicKey.encode(), True)
  62. tokenStore.storeEphemeralPublicKey(ephemeralPublicKeyBase64)
  63. tokenStore.storeToken(medium, address, roomId, sender, token)
  64. # Variables to substitute in the template.
  65. substitutions = {}
  66. # Include all arguments sent via the request.
  67. for k, v in args.items():
  68. if isinstance(v, string_types):
  69. substitutions[k] = v
  70. substitutions["token"] = token
  71. # Substitutions that the template requires, but are optional to provide
  72. # to the API.
  73. extra_substitutions = [
  74. 'sender_display_name',
  75. 'token',
  76. 'room_name',
  77. 'bracketed_room_name',
  78. 'room_avatar_url',
  79. 'sender_avatar_url',
  80. 'guest_user_id',
  81. 'guest_access_token',
  82. ]
  83. for k in extra_substitutions:
  84. substitutions.setdefault(k, '')
  85. substitutions["ephemeral_private_key"] = ephemeralPrivateKeyBase64
  86. if substitutions["room_name"] != '':
  87. substitutions["bracketed_room_name"] = "(%s)" % substitutions["room_name"]
  88. substitutions["web_client_location"] = self.sydent.default_web_client_location
  89. if 'org.matrix.web_client_location' in substitutions:
  90. substitutions["web_client_location"] = substitutions.pop("org.matrix.web_client_location")
  91. subject_header = Header(self.sydent.cfg.get('email', 'email.invite.subject', raw=True) % substitutions, 'utf8')
  92. substitutions["subject_header_value"] = subject_header.encode()
  93. brand = self.sydent.brand_from_request(request)
  94. templateFile = self.sydent.get_branded_template(
  95. brand,
  96. "invite_template.eml",
  97. ('email', 'email.invite_template'),
  98. )
  99. sendEmail(self.sydent, templateFile, address, substitutions)
  100. pubKey = self.sydent.keyring.ed25519.verify_key
  101. pubKeyBase64 = encode_base64(pubKey.encode())
  102. baseUrl = "%s/_matrix/identity/api/v1" % (self.sydent.cfg.get('http', 'client_http_base'),)
  103. keysToReturn = []
  104. keysToReturn.append({
  105. "public_key": pubKeyBase64,
  106. "key_validity_url": baseUrl + "/pubkey/isvalid",
  107. })
  108. keysToReturn.append({
  109. "public_key": ephemeralPublicKeyBase64,
  110. "key_validity_url": baseUrl + "/pubkey/ephemeral/isvalid",
  111. })
  112. resp = {
  113. "token": token,
  114. "public_key": pubKeyBase64,
  115. "public_keys": keysToReturn,
  116. "display_name": self.redact_email_address(address),
  117. }
  118. return resp
  119. def redact_email_address(self, address):
  120. """
  121. Redacts the content of a 3PID address. Redacts both the email's username and
  122. domain independently.
  123. :param address: The address to redact.
  124. :type address: unicode
  125. :return: The redacted address.
  126. :rtype: unicode
  127. """
  128. # Extract strings from the address
  129. username, domain = address.split(u"@", 1)
  130. # Obfuscate strings
  131. redacted_username = self._redact(username, self.sydent.username_obfuscate_characters)
  132. redacted_domain = self._redact(domain, self.sydent.domain_obfuscate_characters)
  133. return redacted_username + u"@" + redacted_domain
  134. def _redact(self, s, characters_to_reveal):
  135. """
  136. Redacts the content of a string, using a given amount of characters to reveal.
  137. If the string is shorter than the given threshold, redact it based on length.
  138. :param s: The string to redact.
  139. :type s: unicode
  140. :param characters_to_reveal: How many characters of the string to leave before
  141. the '...'
  142. :type characters_to_reveal: int
  143. :return: The redacted string.
  144. :rtype: unicode
  145. """
  146. # If the string is shorter than the defined threshold, redact based on length
  147. if len(s) <= characters_to_reveal:
  148. if len(s) > 5:
  149. return s[:3] + u"..."
  150. if len(s) > 1:
  151. return s[0] + u"..."
  152. return u"..."
  153. # Otherwise truncate it and add an ellipses
  154. return s[:characters_to_reveal] + u"..."
  155. def _randomString(self, length):
  156. """
  157. Generate a random string of the given length.
  158. :param length: The length of the string to generate.
  159. :type length: int
  160. :return: The generated string.
  161. :rtype: unicode
  162. """
  163. return u''.join(self.random.choice(string.ascii_letters) for _ in range(length))