msisdnservlet.py 7.5 KB

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