msisdnservlet.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2016 OpenMarket Ltd
  3. # Copyright 2017 Vector Creations 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 logging
  18. from twisted.web.resource import Resource
  19. import phonenumbers
  20. from sydent.validators import (
  21. DestinationRejectedException,
  22. IncorrectClientSecretException,
  23. InvalidSessionIdException,
  24. IncorrectSessionTokenException,
  25. SessionExpiredException,
  26. )
  27. from sydent.http.servlets import get_args, jsonwrap, send_cors
  28. from sydent.http.auth import authIfV2
  29. from sydent.util.stringutils import is_valid_client_secret
  30. logger = logging.getLogger(__name__)
  31. class MsisdnRequestCodeServlet(Resource):
  32. isLeaf = True
  33. def __init__(self, syd):
  34. self.sydent = syd
  35. @jsonwrap
  36. def render_POST(self, request):
  37. send_cors(request)
  38. authIfV2(self.sydent, request)
  39. args = get_args(request, ('phone_number', 'country', 'client_secret', 'send_attempt'))
  40. raw_phone_number = args['phone_number']
  41. country = args['country']
  42. sendAttempt = args['send_attempt']
  43. clientSecret = args['client_secret']
  44. if not is_valid_client_secret(clientSecret):
  45. request.setResponseCode(400)
  46. return {
  47. 'errcode': 'M_INVALID_PARAM',
  48. 'error': 'Invalid client_secret provided'
  49. }
  50. try:
  51. phone_number_object = phonenumbers.parse(raw_phone_number, country)
  52. except Exception as e:
  53. logger.warn("Invalid phone number given: %r", e)
  54. request.setResponseCode(400)
  55. return {'errcode': 'M_INVALID_PHONE_NUMBER', 'error': "Invalid phone number" }
  56. msisdn = phonenumbers.format_number(
  57. phone_number_object, phonenumbers.PhoneNumberFormat.E164
  58. )[1:]
  59. # International formatted number. The same as an E164 but with spaces
  60. # in appropriate places to make it nicer for the humans.
  61. intl_fmt = phonenumbers.format_number(
  62. phone_number_object, phonenumbers.PhoneNumberFormat.INTERNATIONAL
  63. )
  64. brand = self.sydent.brand_from_request(request)
  65. try:
  66. sid = self.sydent.validators.msisdn.requestToken(
  67. phone_number_object, clientSecret, sendAttempt, brand
  68. )
  69. resp = {
  70. 'success': True, 'sid': str(sid),
  71. 'msisdn': msisdn, 'intl_fmt': intl_fmt,
  72. }
  73. except DestinationRejectedException:
  74. logger.error("Destination rejected for number: %s", msisdn)
  75. request.setResponseCode(400)
  76. resp = {'errcode': 'M_DESTINATION_REJECTED', 'error': 'Phone numbers in this country are not currently supported'}
  77. except Exception as e:
  78. logger.error("Exception sending SMS: %r", e)
  79. request.setResponseCode(500)
  80. resp = {'errcode': 'M_UNKNOWN', 'error': 'Internal Server Error'}
  81. return resp
  82. def render_OPTIONS(self, request):
  83. send_cors(request)
  84. return b''
  85. class MsisdnValidateCodeServlet(Resource):
  86. isLeaf = True
  87. def __init__(self, syd):
  88. self.sydent = syd
  89. def render_GET(self, request):
  90. send_cors(request)
  91. err, args = get_args(request, ('token', 'sid', 'client_secret'))
  92. if err:
  93. msg = "Verification failed: Your request was invalid."
  94. else:
  95. resp = self.do_validate_request(args)
  96. if 'success' in resp and resp['success']:
  97. msg = "Verification successful! Please return to your Matrix client to continue."
  98. if 'next_link' in args:
  99. next_link = args['next_link']
  100. request.setResponseCode(302)
  101. request.setHeader("Location", next_link)
  102. else:
  103. request.setResponseCode(400)
  104. msg = "Verification failed: you may need to request another verification text"
  105. brand = self.sydent.brand_from_request(request)
  106. templateFile = self.sydent.get_branded_template(
  107. brand,
  108. "verify_response_template.html",
  109. ('http', 'verify_response_template'),
  110. )
  111. request.setHeader("Content-Type", "text/html")
  112. return open(templateFile).read() % {'message': msg}
  113. @jsonwrap
  114. def render_POST(self, request):
  115. send_cors(request)
  116. authIfV2(self.sydent, request)
  117. return self.do_validate_request(request)
  118. def do_validate_request(self, request):
  119. """
  120. Extracts information about a validation session from the request and
  121. attempts to validate that session.
  122. :param request: The request to extract information about the session from.
  123. :type request: twisted.web.server.Request
  124. :return: A dict with a "success" key which value indicates whether the
  125. validation succeeded. If the validation failed, this dict also includes
  126. a "errcode" and a "error" keys which include information about the failure.
  127. :rtype: dict[str, bool or str]
  128. """
  129. args = get_args(request, ('token', 'sid', 'client_secret'))
  130. sid = args['sid']
  131. tokenString = args['token']
  132. clientSecret = args['client_secret']
  133. if not is_valid_client_secret(clientSecret):
  134. request.setResponseCode(400)
  135. return {
  136. 'errcode': 'M_INVALID_PARAM',
  137. 'error': 'Invalid client_secret provided'
  138. }
  139. try:
  140. return self.sydent.validators.msisdn.validateSessionWithToken(sid, clientSecret, tokenString)
  141. except IncorrectClientSecretException:
  142. request.setResponseCode(400)
  143. return {'success': False, 'errcode': 'M_INVALID_PARAM',
  144. 'error': "Client secret does not match the one given when requesting the token"}
  145. except SessionExpiredException:
  146. request.setResponseCode(400)
  147. return {'success': False, 'errcode': 'M_SESSION_EXPIRED',
  148. 'error': "This validation session has expired: call requestToken again"}
  149. except InvalidSessionIdException:
  150. request.setResponseCode(400)
  151. return {'success': False, 'errcode': 'M_INVALID_PARAM',
  152. 'error': "The token doesn't match"}
  153. except IncorrectSessionTokenException:
  154. request.setResponseCode(404)
  155. return {'success': False, 'errcode': 'M_NO_VALID_SESSION',
  156. 'error': "No session could be found with this sid"}
  157. def render_OPTIONS(self, request):
  158. send_cors(request)
  159. return b''