From 5382c027a9d566a6f49420109db439f9b4d99e53 Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Fri, 10 Nov 2023 11:33:24 -0300 Subject: [PATCH 1/6] add RequestValidator --- .gitignore | 1 + signalwire/request_validator.py | 47 +++++++++ signalwire/tests/test_request_validator.py | 111 +++++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 signalwire/request_validator.py create mode 100644 signalwire/tests/test_request_validator.py diff --git a/.gitignore b/.gitignore index 65066cb..49a719f 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ Dockerfile.dev tmp *.pyc .vscode +.devcontainer diff --git a/signalwire/request_validator.py b/signalwire/request_validator.py new file mode 100644 index 0000000..f7d1140 --- /dev/null +++ b/signalwire/request_validator.py @@ -0,0 +1,47 @@ +import base64 +import hmac +from hashlib import sha1 +from urllib.parse import urlparse +from twilio.request_validator import compare, remove_port, add_port, RequestValidator as TwilioRequestValidator + + +class RequestValidator(object): + def __init__(self, token): + self.compatibility_validator = TwilioRequestValidator(token) + self.token = token.encode() + + def build_signature_with_compatibility(self, uri, params): + return self.compatibility_validator.compute_signature(uri, params) + + def validate_with_compatibility(self, uri, params, signature): + return self.compatibility_validator.validate(uri, params, signature) + + def build_signature(self, uri, params): + s = uri + if params: + s += params + + hmac_buffer = hmac.new(self.token, s.encode(), sha1) + result = hmac_buffer.digest().hex() + + return result.strip() + + def validate(self, uri, params, signature): + + if isinstance(params, str): + parsed_uri = urlparse(uri) + with_port = add_port(parsed_uri) + without_port = remove_port(parsed_uri) + + valid_signature_without_port = compare( + self.build_signature(without_port, params), + signature + ) + valid_signature_with_port = compare( + self.build_signature(with_port, params), + signature + ) + + return valid_signature_without_port or valid_signature_with_port + + return self.validate_with_compatibility(uri, params, signature) diff --git a/signalwire/tests/test_request_validator.py b/signalwire/tests/test_request_validator.py new file mode 100644 index 0000000..2daac22 --- /dev/null +++ b/signalwire/tests/test_request_validator.py @@ -0,0 +1,111 @@ +from unittest import TestCase +from multidict import MultiDict + +class TestRequestValidator(TestCase): + def test_should_validate_no_compatibility(self): + from signalwire.request_validator import RequestValidator + + url = 'https://81f2-2-45-18-191.ngrok-free.app/' + token = 'PSK_7TruNcSNTxp4zNrykMj4EPzF' + signature = 'b18500437ebb010220ddd770cbe6fd531ea0ba0d' + body = '{"call":{"call_id":"b5d63b2e-f75b-4dc8-b6d4-269b635f96c0","node_id":"fa3570ae-f8bd-42c2-83f4-9950d906c91b@us-west","segment_id":"b5d63b2e-f75b-4dc8-b6d4-269b635f96c0","call_state":"created","direction":"inbound","type":"phone","from":"+12135877632","to":"+12089806814","from_number":"+12135877632","to_number":"+12089806814","project_id":"4b7ae78a-d02e-4889-a63b-08b156d5916e","space_id":"62615f44-2a34-4235-b38b-76b5a1de6ef8"},"vars":{}}' + + validator = RequestValidator(token) + computed = validator.build_signature(url, body) + self.assertIsInstance(computed, str) + self.assertEqual(signature, computed) + valid = validator.validate(url, body, signature) + self.assertTrue(valid) + + def test_should_validate_with_compatibity(self): + from signalwire.request_validator import RequestValidator + + url = 'https://mycompany.com/myapp.php?foo=1&bar=2' + token = '12345' + signature = 'RSOYDt4T1cUTdK1PDd93/VVr8B8=' + body = { + 'CallSid': 'CA1234567890ABCDE', + 'Caller': '+14158675309', + 'Digits': '1234', + 'From': '+14158675309', + 'To': '+18005551212', + } + + validator = RequestValidator(token) + valid = validator.validate(url, body, signature) + self.assertTrue(valid) + + def test_should_validate_with_compatibity_flask(self): + from signalwire.request_validator import RequestValidator + + url = 'https://mycompany.com/myapp.php?foo=1&bar=2' + token = '12345' + signature = 'RSOYDt4T1cUTdK1PDd93/VVr8B8=' + body = MultiDict ( + [ + ('CallSid', 'CA1234567890ABCDE'), + ('Caller', '+14158675309'), + ('Digits', '1234'), + ('From', '+14158675309'), + ('To', '+18005551212') + ] + ) + + validator = RequestValidator(token) + valid = validator.validate(url, body, signature) + self.assertTrue(valid) + + def test_should_validate_from_signalwire_http_request(self): + from signalwire.request_validator import RequestValidator + + url = 'http://0aac-189-71-169-171.ngrok-free.app/voice' + token = 'PSK_V3bF8oyeRNpJWGoRWHNYQMUU' + signature = 'lf3nWPmUr2y6jSeeoMW4mg58vgI=' #From Lib + body = { + "AccountSid": "6bfbbe86-a901-4197-8759-2a0de1fa319d", + "ApiVersion": "2010-04-01", + "CallbackSource": "call-progress-events", + "CallSid": "0703574f-b151-465d-aedb-28972eb513c7", + "CallStatus": "busy", + "Direction": "outbound-api", + "From": "sip:+17063958228@sip.swire.io", + "HangupBy": "sip:jpsantos@joaosantos-2a0de1fa319d.sip.swire.io", + "HangupDirection": "inbound", + "Timestamp": "Thu, 09 Nov 2023 17:05:04 +0000", + "To": "sip:jpsantos@joaosantos-2a0de1fa319d.sip.swire.io", + "SipResultCode": "486" + } + + validator = RequestValidator(token) + valid = validator.validate(url, body, signature) + self.assertTrue(valid) + + def test_should_validate_from_signalwire_https_request(self): + from signalwire.request_validator import RequestValidator + + url = 'https://675d-189-71-169-171.ngrok-free.app/voice' + token = 'PSK_V3bF8oyeRNpJWGoRWHNYQMUU' + signature = 'muUMpldcBHlzuXGZ5gbw1ETZCYA=' + body = { + "CallSid": "a97d4e8a-6047-4e2b-be48-fb96b33b5642", + "AccountSid": "6bfbbe86-a901-4197-8759-2a0de1fa319d", + "ApiVersion": "2010-04-01", + "Direction": "outbound-api", + "From": "sip:+17063958228@sip.swire.io", + "To": "sip:jpsantos@joaosantos-2a0de1fa319d.sip.swire.io", + "Timestamp": "Thu, 09 Nov 2023 14:40:55 +0000", + "CallStatus": "no-answer", + "CallbackSource": "call-progress-events", + "HangupDirection": "outbound", + "HangupBy": "sip:+17063958228@sip.swire.io", + "SipResultCode": "487" + } + + + validator = RequestValidator(token) + valid = validator.validate(url, body, signature) + self.assertTrue(valid) + + + + From 8fc1b61245bcc1d4dde02053c0444cdf46e09b8e Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Fri, 10 Nov 2023 11:42:45 -0300 Subject: [PATCH 2/6] update image --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a8e34b9..306deed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7-alpine +FROM python:3.10-slim COPY . /app WORKDIR /app From 121b6fab8ea1d00e191652db844b6a7f30fbbd7f Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Fri, 10 Nov 2023 11:46:01 -0300 Subject: [PATCH 3/6] use bookworm distro --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 306deed..04fe2c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10-slim +FROM python:3.10-bookworm COPY . /app WORKDIR /app From 81d2473188dcd32a23d4bd64e4baf61edcf6e338 Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Fri, 10 Nov 2023 12:09:53 -0300 Subject: [PATCH 4/6] setup gha --- .drone.yml | 14 -------------- .github/workflows/ci.yml | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 14 deletions(-) delete mode 100644 .drone.yml create mode 100644 .github/workflows/ci.yml diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 529732c..0000000 --- a/.drone.yml +++ /dev/null @@ -1,14 +0,0 @@ -kind: pipeline -name: default - -steps: -- name: test - image: python:3.7-alpine - commands: - - apk add --no-cache --update python3-dev gcc build-base - - pip install pipenv - - pipenv install --dev - - pipenv run pytest - -trigger: - event: push diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..44dce63 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: Run Test + +on: + push: + branches: [ master ] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + - name: Build Image + run: docker build -t signalwire-python-sdk . + - name: Run Tests + run: docker run signalwire-python-sdk \ No newline at end of file From 1a0883c2b3ee9bfc93c32da23eb038a982a8efc5 Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Fri, 10 Nov 2023 12:28:19 -0300 Subject: [PATCH 5/6] use slim --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 04fe2c7..306deed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10-bookworm +FROM python:3.10-slim COPY . /app WORKDIR /app From ca6fd457af4475441602ad29e46c3482a970d9a3 Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Tue, 14 Nov 2023 12:03:20 -0300 Subject: [PATCH 6/6] handles a JSON body that isn't a SWML callback --- signalwire/request_validator.py | 10 ++++++++- signalwire/tests/test_request_validator.py | 26 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/signalwire/request_validator.py b/signalwire/request_validator.py index f7d1140..2021d39 100644 --- a/signalwire/request_validator.py +++ b/signalwire/request_validator.py @@ -1,5 +1,6 @@ import base64 import hmac +import json from hashlib import sha1 from urllib.parse import urlparse from twilio.request_validator import compare, remove_port, add_port, RequestValidator as TwilioRequestValidator @@ -42,6 +43,13 @@ def validate(self, uri, params, signature): signature ) - return valid_signature_without_port or valid_signature_with_port + if valid_signature_without_port or valid_signature_with_port: + return True + + try: + parsed_params = json.loads(params) + return self.validate_with_compatibility(uri, parsed_params, signature) + except json.JSONDecodeError as e: + return False return self.validate_with_compatibility(uri, params, signature) diff --git a/signalwire/tests/test_request_validator.py b/signalwire/tests/test_request_validator.py index 2daac22..1be2fe3 100644 --- a/signalwire/tests/test_request_validator.py +++ b/signalwire/tests/test_request_validator.py @@ -106,6 +106,32 @@ def test_should_validate_from_signalwire_https_request(self): valid = validator.validate(url, body, signature) self.assertTrue(valid) + def test_should_validate_from_raw_json(self): + from signalwire.request_validator import RequestValidator + + url = 'https://675d-189-71-169-171.ngrok-free.app/voice' + token = 'PSK_V3bF8oyeRNpJWGoRWHNYQMUU' + signature = 'muUMpldcBHlzuXGZ5gbw1ETZCYA=' + body = '''{ + "CallSid": "a97d4e8a-6047-4e2b-be48-fb96b33b5642", + "AccountSid": "6bfbbe86-a901-4197-8759-2a0de1fa319d", + "ApiVersion": "2010-04-01", + "Direction": "outbound-api", + "From": "sip:+17063958228@sip.swire.io", + "To": "sip:jpsantos@joaosantos-2a0de1fa319d.sip.swire.io", + "Timestamp": "Thu, 09 Nov 2023 14:40:55 +0000", + "CallStatus": "no-answer", + "CallbackSource": "call-progress-events", + "HangupDirection": "outbound", + "HangupBy": "sip:+17063958228@sip.swire.io", + "SipResultCode": "487" + }''' + + + validator = RequestValidator(token) + valid = validator.validate(url, body, signature) + self.assertTrue(valid) +