msisdnvalidator.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  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. import logging
  17. import urllib
  18. import phonenumbers
  19. from sydent.db.valsession import ThreePidValSessionStore
  20. from sydent.validators import ValidationSession, common
  21. from sydent.sms.openmarket import OpenMarketSMS
  22. from sydent.validators import DestinationRejectedException
  23. from sydent.util import time_msec
  24. logger = logging.getLogger(__name__)
  25. class MsisdnValidator:
  26. def __init__(self, sydent):
  27. self.sydent = sydent
  28. self.omSms = OpenMarketSMS(sydent)
  29. # cache originators & sms rules from config file
  30. self.originators = {}
  31. self.smsRules = {}
  32. for opt in self.sydent.cfg.options('sms'):
  33. if opt.startswith('originators.'):
  34. country = opt.split('.')[1]
  35. rawVal = self.sydent.cfg.get('sms', opt)
  36. rawList = [i.strip() for i in rawVal.split(',')]
  37. self.originators[country] = []
  38. for origString in rawList:
  39. parts = origString.split(':')
  40. if len(parts) != 2:
  41. raise Exception("Originators must be in form: long:<number>, short:<number> or alpha:<text>, separated by commas")
  42. if parts[0] not in ['long', 'short', 'alpha']:
  43. raise Exception("Invalid originator type: valid types are long, short and alpha")
  44. self.originators[country].append({
  45. "type": parts[0],
  46. "text": parts[1],
  47. })
  48. elif opt.startswith('smsrule.'):
  49. country = opt.split('.')[1]
  50. action = self.sydent.cfg.get('sms', opt)
  51. if action not in ['allow', 'reject']:
  52. raise Exception("Invalid SMS rule action: %s, expecting 'allow' or 'reject'" % action)
  53. self.smsRules[country] = action
  54. def requestToken(self, phoneNumber, clientSecret, sendAttempt, nextLink):
  55. if str(phoneNumber.country_code) in self.smsRules:
  56. action = self.smsRules[str(phoneNumber.country_code)]
  57. if action == 'reject':
  58. raise DestinationRejectedException()
  59. valSessionStore = ThreePidValSessionStore(self.sydent)
  60. msisdn = phonenumbers.format_number(
  61. phoneNumber, phonenumbers.PhoneNumberFormat.E164
  62. )[1:]
  63. valSession = valSessionStore.getOrCreateTokenSession(
  64. medium='msisdn', address=msisdn, clientSecret=clientSecret
  65. )
  66. valSessionStore.setMtime(valSession.id, time_msec())
  67. if int(valSession.sendAttemptNumber) >= int(sendAttempt):
  68. logger.info("Not texting code because current send attempt (%d) is not less than given send attempt (%s)", int(sendAttempt), int(valSession.sendAttemptNumber))
  69. return valSession.id
  70. smsBodyTemplate = self.sydent.cfg.get('sms', 'bodyTemplate')
  71. originator = self.getOriginator(phoneNumber)
  72. logger.info(
  73. "Attempting to text code %s to %s (country %d) with originator %s",
  74. valSession.token, msisdn, phoneNumber.country_code, originator
  75. )
  76. smsBody = smsBodyTemplate.format(token=valSession.token)
  77. self.omSms.sendTextSMS(smsBody, msisdn, originator)
  78. valSessionStore.setSendAttemptNumber(valSession.id, sendAttempt)
  79. return valSession.id
  80. def getOriginator(self, destPhoneNumber):
  81. countryCode = str(destPhoneNumber.country_code)
  82. origs = [{
  83. "type": "alpha",
  84. "text": "Matrix",
  85. }]
  86. if countryCode in self.originators:
  87. origs = self.originators[countryCode]
  88. elif 'default' in self.originators:
  89. origs = self.originators['default']
  90. # deterministically pick an originator from the list of possible
  91. # originators, so if someone requests multiple codes, they come from
  92. # a consistent number (if there's any chance that some originators are
  93. # more likley to work than others, we may want to change, but it feels
  94. # like this should be something other than just picking one randomly).
  95. msisdn = phonenumbers.format_number(
  96. destPhoneNumber, phonenumbers.PhoneNumberFormat.E164
  97. )[1:]
  98. return origs[sum([int(i) for i in msisdn]) % len(origs)]
  99. def validateSessionWithToken(self, sid, clientSecret, token):
  100. return common.validateSessionWithToken(self.sydent, sid, clientSecret, token)