class ValidationTest(unittest.TestCase): def setUp(self): token = "12345" self.validator = RequestValidator(token) self.uri = "https://mycompany.com/myapp.php?foo=1&bar=2" self.params = { "CallSid": "CA1234567890ABCDE", "Digits": "1234", "From": "+14158675309", "To": "+18005551212", "Caller": "+14158675309", } self.expected = "RSOYDt4T1cUTdK1PDd93/VVr8B8=" self.body = "{\"property\": \"value\", \"boolean\": true}" self.bodyHash = "Ch/3Y02as7ldtcmi3+lBbkFQKyg6gMfPGWMmMvluZiA=" self.encodedBodyHash = self.bodyHash.replace("+", "%2B").replace( "=", "%3D") self.uriWithBody = self.uri + "&bodySHA256=" + self.encodedBodyHash def test_compute_signature_bytecode(self): expected = b(self.expected) signature = self.validator.compute_signature(self.uri, self.params, utf=False) assert_equal(signature, expected) def test_compute_signature_unicode(self): expected = u(self.expected) signature = self.validator.compute_signature(self.uri, self.params, utf=True) assert_equal(signature, expected) def test_compute_hash_bytecode(self): expected = b(self.bodyHash) body_hash = self.validator.compute_hash(self.body, utf=False) assert_equal(expected, body_hash) def test_compute_hash_unicode(self): expected = u(self.bodyHash) body_hash = self.validator.compute_hash(self.body, utf=True) assert_equal(expected, body_hash) def test_validation(self): assert_true( self.validator.validate(self.uri, self.params, self.expected)) def test_validation_removes_port_on_https(self): uri = self.uri.replace(".com", ".com:1234") assert_true(self.validator.validate(uri, self.params, self.expected)) def test_validation_of_body_succeeds(self): uri = self.uriWithBody is_valid = self.validator.validate(uri, self.body, "afcFvPLPYT8mg/JyIVkdnqQKa2s=") assert_true(is_valid)
class ValidationTest(unittest.TestCase): def setUp(self): token = "1c892n40nd03kdnc0112slzkl3091j20" self.validator = RequestValidator(token) self.uri = "http://www.postbin.org/1ed898x" self.params = { "AccountSid": "AC9a9f9392lad99kla0sklakjs90j092j3", "ApiVersion": "2010-04-01", "CallSid": "CAd800bb12c0426a7ea4230e492fef2a4f", "CallStatus": "ringing", "Called": "+15306384866", "CalledCity": "OAKLAND", "CalledCountry": "US", "CalledState": "CA", "CalledZip": "94612", "Caller": "+15306666666", "CallerCity": "SOUTH LAKE TAHOE", "CallerCountry": "US", "CallerName": "CA Wireless Call", "CallerState": "CA", "CallerZip": "89449", "Direction": "inbound", "From": "+15306666666", "FromCity": "SOUTH LAKE TAHOE", "FromCountry": "US", "FromState": "CA", "FromZip": "89449", "To": "+15306384866", "ToCity": "OAKLAND", "ToCountry": "US", "ToState": "CA", "ToZip": "94612", } def test_compute_signature_bytecode(self): expected = b("fF+xx6dTinOaCdZ0aIeNkHr/ZAA=") signature = self.validator.compute_signature(self.uri, self.params, utf=False) assert_equal(signature, expected) def test_compute_signature_unicode(self): expected = u("fF+xx6dTinOaCdZ0aIeNkHr/ZAA=") signature = self.validator.compute_signature(self.uri, self.params, utf=True) assert_equal(signature, expected) def test_validation(self): expected = "fF+xx6dTinOaCdZ0aIeNkHr/ZAA=" assert_true(self.validator.validate(self.uri, self.params, expected)) def test_validation_removes_port_on_https(self): self.uri = "https://www.postbin.org:1234/1ed898x" expected = "Y7MeICc5ECftd1G11Fc8qoxAn0A=" assert_true(self.validator.validate(self.uri, self.params, expected))
class ValidationTest(unittest.TestCase): def setUp(self): token = "12345" self.validator = RequestValidator(token) self.uri = "https://mycompany.com/myapp.php?foo=1&bar=2" self.params = { "CallSid": "CA1234567890ABCDE", "Digits": "1234", "From": "+14158675309", "To": "+18005551212", "Caller": "+14158675309", } self.expected = "RSOYDt4T1cUTdK1PDd93/VVr8B8=" self.body = "{\"property\": \"value\", \"boolean\": true}" self.bodyHash = "Ch/3Y02as7ldtcmi3+lBbkFQKyg6gMfPGWMmMvluZiA=" self.encodedBodyHash = self.bodyHash.replace("+", "%2B").replace("=", "%3D") self.uriWithBody = self.uri + "&bodySHA256=" + self.encodedBodyHash def test_compute_signature_bytecode(self): expected = b(self.expected) signature = self.validator.compute_signature(self.uri, self.params, utf=False) assert_equal(signature, expected) def test_compute_signature_unicode(self): expected = u(self.expected) signature = self.validator.compute_signature(self.uri, self.params, utf=True) assert_equal(signature, expected) def test_compute_hash_bytecode(self): expected = b(self.bodyHash) body_hash = self.validator.compute_hash(self.body, utf=False) assert_equal(expected, body_hash) def test_compute_hash_unicode(self): expected = u(self.bodyHash) body_hash = self.validator.compute_hash(self.body, utf=True) assert_equal(expected, body_hash) def test_validation(self): assert_true(self.validator.validate(self.uri, self.params, self.expected)) def test_validation_removes_port_on_https(self): uri = self.uri.replace(".com", ".com:1234") assert_true(self.validator.validate(uri, self.params, self.expected)) def test_validation_of_body_succeeds(self): uri = self.uriWithBody is_valid = self.validator.validate(uri, self.body, "afcFvPLPYT8mg/JyIVkdnqQKa2s=") assert_true(is_valid)
class ValidationTest(unittest.TestCase): def setUp(self): token = "12345" self.validator = RequestValidator(token) self.uri = "https://mycompany.com/myapp.php?foo=1&bar=2" self.params = { "CallSid": "CA1234567890ABCDE", "Digits": "1234", "From": "+14158675309", "To": "+18005551212", "Caller": "+14158675309", } self.expected = "RSOYDt4T1cUTdK1PDd93/VVr8B8=" self.body = "{\"property\": \"value\", \"boolean\": true}" self.bodyHash = "0a1ff7634d9ab3b95db5c9a2dfe9416e41502b283a80c7cf19632632f96e6620" self.uriWithBody = self.uri + "&bodySHA256=" + self.bodyHash def test_compute_signature_bytecode(self): expected = b(self.expected) signature = self.validator.compute_signature(self.uri, self.params, utf=False) assert_equal(signature, expected) def test_compute_signature(self): expected = (self.expected) signature = self.validator.compute_signature(self.uri, self.params, utf=True) assert_equal(signature, expected) def test_compute_hash_unicode(self): expected = u(self.bodyHash) body_hash = self.validator.compute_hash(self.body) assert_equal(expected, body_hash) def test_validation(self): assert_true( self.validator.validate(self.uri, self.params, self.expected)) def test_validation_removes_port_on_https(self): uri = self.uri.replace(".com", ".com:1234") assert_true(self.validator.validate(uri, self.params, self.expected)) def test_validation_of_body_succeeds(self): uri = self.uriWithBody is_valid = self.validator.validate(uri, self.body, "a9nBmqA0ju/hNViExpshrM61xv4=") assert_true(is_valid)
class ValidationTest(unittest.TestCase): def setUp(self): token = "12345" self.validator = RequestValidator(token) self.uri = "https://mycompany.com/myapp.php?foo=1&bar=2" self.params = { "CallSid": "CA1234567890ABCDE", "Digits": "1234", "From": "+14158675309", "To": "+18005551212", "Caller": "+14158675309", } self.expected = "RSOYDt4T1cUTdK1PDd93/VVr8B8=" self.body = "{\"property\": \"value\", \"boolean\": true}" self.bodyHash = "0a1ff7634d9ab3b95db5c9a2dfe9416e41502b283a80c7cf19632632f96e6620" self.uriWithBody = self.uri + "&bodySHA256=" + self.bodyHash def test_compute_signature_bytecode(self): expected = b(self.expected) signature = self.validator.compute_signature(self.uri, self.params, utf=False) assert_equal(signature, expected) def test_compute_signature(self): expected = (self.expected) signature = self.validator.compute_signature(self.uri, self.params, utf=True) assert_equal(signature, expected) def test_compute_hash_unicode(self): expected = u(self.bodyHash) body_hash = self.validator.compute_hash(self.body) assert_equal(expected, body_hash) def test_validation(self): assert_true(self.validator.validate(self.uri, self.params, self.expected)) def test_validation_removes_port_on_https(self): uri = self.uri.replace(".com", ".com:1234") assert_true(self.validator.validate(uri, self.params, self.expected)) def test_validation_of_body_succeeds(self): uri = self.uriWithBody is_valid = self.validator.validate(uri, self.body, "a9nBmqA0ju/hNViExpshrM61xv4=") assert_true(is_valid)
class TwilioClient: def __init__(self, account_sid, auth_token): self.client = Client(account_sid, auth_token) self.request_validator = RequestValidator(auth_token) def compute_signature(self, uri, params): """proxy twilio.RequestValidator.compute_signature()""" return self.request_validator.compute_signature(uri, params) def validate_request(self, uri, params, signature): """proxy twilio.RequestValidator.validate()""" return self.request_validator.validate(uri, params, signature) def send_sms(self, to="", from_="", body=""): try: message = self.client.messages.create( to=to, from_=from_, body=body ) except TwilioRestException: # TODO log this original exception for debugging raise TwilioClientException('Failed to send SMS') return message
def call(self, to, path="/voice/", from_="+15556667777", extra_params=None): params = { "CallSid": "CAtesting", "AccountSid": "ACxxxxx", "To": to, "From": from_, "Direction": "inbound", "FromCity": "BROOKLYN", "FromState": "NY", "FromCountry": "US", "FromZip": "55555" } if extra_params: for k, v in extra_params.items(): params[k] = v HTTP_HOST = "example.com" validator = RequestValidator("yyyyyyyy") absolute_url = "http://{0}{1}".format(HTTP_HOST, path) signature = validator.compute_signature(absolute_url, params) return self.post(path, params, HTTP_X_TWILIO_SIGNATURE=signature, HTTP_HOST=HTTP_HOST)
def test_mms(client, contact_table, unit_table, mock_viper): # MMS messages in Australia are delivered to twilio as an SMS with # a link. # Below is a captured Telstra message, delivered via Twilio originating # from an Optus network mobile. I am unsure if they will all be in the # Telstra format. # Desired behaviour is to send an error SMS reply, do not send a page. telstra_mms = "You have a new MMS Picture or Video Message. To view your message, go to http://telstra.com/mmsview & enter User ID:61437867737 and Password:048jnd within 14 days" data = {'Body': telstra_mms, 'From': '123'} token = 'ABC' page.os.environ['TWILIO_AUTH_TOKEN'] = token validator = RequestValidator(token) sig = validator.compute_signature('http://localhost/', data) headers = {'X-TWILIO-SIGNATURE': sig} contact_table.put_item({ 'phone_number': { 'S': '123' }, 'unit': { 'S': 'TestData' }, 'member_id': { 'N': 12345 }, }) unit_table.put_item({'name': {'S': 'TestData'}, 'capcode': {'N': 1111}}) bounce = client.post('/', data=data, headers=headers) assert (bounce.status_code == 200) assert (b"This service does not support MMS messages" in bounce.data) assert (mock_viper.mock_calls == [])
def test_twilio_check(client): token = "AABBCCDD" data = {'Body': 'Twilio check body', 'From': '1234567890'} # Monkey patch the app's environment hash page.os.environ['TWILIO_AUTH_TOKEN'] = token validator = RequestValidator(token) sig = validator.compute_signature('http://localhost/', data) # TODO: Incoming number is +61... # Good request # Response will be an TWIML error response # Don't care, Twilio auth check passed good_headers = {'X-TWILIO-SIGNATURE': sig} good = client.post('/', data=data, headers=good_headers) assert (good.status_code == 200) # Request missing headers missing = client.post('/', data=data, headers={}) assert (missing.status_code == 403) # Request with bad header bad_headers = {'X-TWILIO-SIGNATURE': 'RABBITHAT'} bad = client.post('/', data=data, headers=bad_headers) assert (bad.status_code == 403) # Request signature with bad url sig = validator.compute_signature('http://otherhost/', data) bad_url_headers = {'X-TWILIO-SIGNATURE': sig} bad_url = client.post('/', data=data, headers=bad_url_headers) assert (bad_url.status_code == 403) # Request signature with bad data extra_data = data.copy() extra_data['flubble'] = 'wubble' sig = validator.compute_signature('http://localhost/', extra_data) bad_data_headers = {'X-TWILIO-SIGNATURE': sig} bad_data = client.post('/', data=data, headers=bad_data_headers) assert (bad_data.status_code == 403) # Request signature with messed up token page.os.environ[ 'TWILIO_AUTH_TOKEN'] = "I laugh at them because they're all the same" bad_token = client.post('/', data=data, headers=good_headers) assert (bad_token.status_code == 403)
class ValidationTest(unittest.TestCase): def setUp(self): token = "12345" self.validator = RequestValidator(token) self.uri = "https://mycompany.com/myapp.php?foo=1&bar=2" self.params = { "CallSid": "CA1234567890ABCDE", "Digits": "1234", "From": "+14158675309", "To": "+18005551212", "Caller": "+14158675309", } self.expected = "RSOYDt4T1cUTdK1PDd93/VVr8B8=" self.body = "{\"property\": \"value\", \"boolean\": true}" self.bodyHash = "0a1ff7634d9ab3b95db5c9a2dfe9416e41502b283a80c7cf19632632f96e6620" self.uriWithBody = self.uri + "&bodySHA256=" + self.bodyHash def test_compute_signature(self): expected = (self.expected) signature = self.validator.compute_signature(self.uri, self.params) assert_equal(signature, expected) def test_compute_hash_unicode(self): expected = self.bodyHash body_hash = self.validator.compute_hash(self.body) assert_equal(expected, body_hash) def test_validation(self): assert_true( self.validator.validate(self.uri, self.params, self.expected)) def test_validation_removes_port_on_https(self): uri = self.uri.replace(".com", ".com:1234") assert_true(self.validator.validate(uri, self.params, self.expected)) def test_validation_removes_port_on_http(self): expected = "Zmvh+3yNM1Phv2jhDCwEM3q5ebU=" # hash of http uri with port 1234 uri = self.uri.replace(".com", ".com:1234").replace("https", "http") assert_true(self.validator.validate(uri, self.params, expected)) def test_validation_adds_port_on_https(self): expected = "kvajT1Ptam85bY51eRf/AJRuM3w=" # hash of uri with port 443 assert_true(self.validator.validate(self.uri, self.params, expected)) def test_validation_adds_port_on_http(self): uri = self.uri.replace("https", "http") expected = "0ZXoZLH/DfblKGATFgpif+LLRf4=" # hash of uri with port 80 assert_true(self.validator.validate(uri, self.params, expected)) def test_validation_of_body_succeeds(self): uri = self.uriWithBody is_valid = self.validator.validate(uri, self.body, "a9nBmqA0ju/hNViExpshrM61xv4=") assert_true(is_valid)
def lookup(self, to, from_, params, path="/lookup/", extra_params=None): if extra_params: for k, v in extra_params.items(): params[k] = v HTTP_HOST = "example.com" validator = RequestValidator("yyyyyyyy") absolute_url = "http://{0}{1}".format(HTTP_HOST, path) signature = validator.compute_signature(absolute_url, params) return self.post(path, params, HTTP_X_TWILIO_SIGNATURE=signature, HTTP_HOST=HTTP_HOST)
def test_db_lookups(client, contact_table, unit_table, mock_viper): data = {'Body': 'Twilio check body', 'From': '1234567890'} token = "AABBCCDD" page.os.environ['TWILIO_AUTH_TOKEN'] = token validator = RequestValidator(token) sig = validator.compute_signature('http://localhost/', data) headers = {'X-TWILIO-SIGNATURE': sig} not_auth = client.post('/', data=data, headers=headers) assert (not_auth.status_code == 200) assert (b"You are not authorised to use this service" in not_auth.data) contact_table.put_item({ 'phone_number': { 'S': '1234567890' }, 'unit': { 'S': 'TestData' }, 'name': { 'S': 'John Smith' }, 'member_id': { 'N': 12345 }, 'permissions': { 'S': '{}' } }) not_unit = client.post('/', data=data, headers=headers) assert (not_unit.status_code == 200) assert (b"You are not authorised to use this service" not in not_unit.data) assert (b"Error retrieving unit details" in not_unit.data) assert (mock_viper.mock_calls == []) unit_table.put_item({'name': {'S': 'TestData'}, 'capcode': {'N': 1111}}) success = client.post('/', data=data, headers=headers) assert (success.status_code == 204) assert (b"You are not authorised to use this service" not in success.data) assert (b"Error retrieving unit details" not in success.data) assert (len(success.data) == 0) expected_calls = [ call(ses_id=None, ses_password=None), call().send(1111, data['Body']) ] assert (mock_viper.mock_calls == expected_calls)
class TwilioMuxer: def __init__(self, twilio_auth_token: str, muxer_url: str, config: Config): self.validator = RequestValidator(twilio_auth_token) self.muxer_url = muxer_url self.config = config def mux_request( self, request_body: str, request_headers: Dict[str, str]) -> Tuple[int, str, Dict[str, str]]: parsed_body = dict(parse_qsl(request_body, keep_blank_values=True)) request_valid = self.validator.validate( self.muxer_url, parsed_body, request_headers.get("x-twilio-signature", request_headers.get("X-Twilio-Signature")), ) if not request_valid: raise RuntimeError(f"Invalid Twilio signature") request_body_normalized = " ".join( parsed_body.get("Body", "").translate( str.maketrans("", "", string.punctuation)).strip().lower().split()) for keyword, config in self.config.keywords.items(): if keyword == request_body_normalized: print(f"Normalized {request_body_normalized} -> {keyword}") elif config.alternates and request_body_normalized in config.alternates: print( f"Normalized alternate {request_body_normalized} -> {keyword}" ) else: continue request_body_normalized = keyword # clean up downstream request too parsed_body["Body"] = keyword break request_config = self.config.keywords.get(request_body_normalized, self.config.default) def make_downstream_request(url: str) -> Optional[requests.Response]: downstream_headers = { k: v for k, v in request_headers.items() if k.lower() in PRESERVE_HEADERS } downstream_headers[ "X-Twilio-Signature"] = self.validator.compute_signature( url, parsed_body) try: result = requests.post(url, data=parsed_body, headers=downstream_headers) except Exception as e: logging.exception(f"Request failed to downstream {url}") sentry_sdk.capture_exception(e) return None try: result.raise_for_status() except Exception as e: logging.exception( f"Request to downstream {url} return status code {result.status_code}" ) sentry_sdk.capture_exception(e) # We return result whether or not raise_for_status() errored -- we're # just doing raise_for_status so we can capture errors; we always want # to return the result return result print(f"Making requests to downstreams: {request_config.downstreams}") with concurrent.futures.ThreadPoolExecutor() as executor: results = list( executor.map(make_downstream_request, request_config.downstreams)) print(f"Downstream responses: {results}") print(f"Taking result from responder: {request_config.responder}") if request_config.responder is None: return 200, "<Response></Response>", { "Content-Type": "application/xml" } result = results[request_config.responder] if result is None: return 500, "<Response></Response>", { "Content-Type": "application/xml" } return ( result.status_code, result.text, { "Content-Type": result.headers.get("Content-Type", "application/xml") }, )