From e7840a636458db2b68f0fc3c2db6188f1fda0418 Mon Sep 17 00:00:00 2001 From: Igor Solyony Date: Wed, 29 May 2019 19:06:03 +0200 Subject: [PATCH 1/3] Validate raw content instead of flatdict --- filestack/models/filestack_client.py | 24 +++++++------------- requirements.txt | 1 - setup.py | 2 +- tests/client_test.py | 33 ++++++++-------------------- 4 files changed, 18 insertions(+), 42 deletions(-) diff --git a/filestack/models/filestack_client.py b/filestack/models/filestack_client.py index da10df6..f9510ce 100644 --- a/filestack/models/filestack_client.py +++ b/filestack/models/filestack_client.py @@ -6,8 +6,6 @@ import requests import mimetypes -from flatdict import FlatterDict - import filestack.models from filestack.config import API_URL, CDN_URL, STORE_PATH, HEADERS @@ -204,14 +202,15 @@ def upload(self, url=None, filepath=None, multipart=True, params=None, upload_pr def validate_webhook_signature(secret, body, headers=None): """ Checks if webhook, which you received was originally from Filestack, - based on you secret for webhook endpoint which was generated in Filestack developer portal + based on you secret for webhook endpoint which was generated in Filestack developer portal. + Body suppose to be raw content of received webhook returns [Dict] ```python from filestack import Client result = client.validate_webhook_signature( - 'secret', {'webhook_content': 'received_from_filestack'}, + 'secret', b'{"webhook_content": "received_from_filestack"}', {'FS-Timestamp': '1558367878', 'FS-Signature': 'Filestack Signature'} ) ``` @@ -229,21 +228,14 @@ def validate_webhook_signature(secret, body, headers=None): if error: return {'error': error, 'valid': True} - cleaned_dict = Client.cleanup_webhook_dict(body) + if isinstance(body, bytes): + body = body.decode('latin-1') - sign = "%s.%s" % (headers_prepared['fs-timestamp'], json.dumps(cleaned_dict, sort_keys=True)) + sign = "%s.%s" % (headers_prepared['fs-timestamp'], body) signature = hmac.new(secret.encode('latin-1'), sign.encode('latin-1'), hashlib.sha256).hexdigest() return {'error': None, 'valid': signature == headers_prepared['fs-signature']} - @staticmethod - def cleanup_webhook_dict(data): - cleaned_dict = dict(FlatterDict(data)) - for k in list(cleaned_dict.keys()): - if isinstance(cleaned_dict[k], FlatterDict): - del cleaned_dict[k] - return cleaned_dict - @staticmethod def validate_webhook_params(secret, body, headers): error = None @@ -251,8 +243,8 @@ def validate_webhook_params(secret, body, headers): error = 'Missing secret or secret is not a string' if not headers or not isinstance(headers, dict): error = 'Missing headers or headers are not a dict' - if not body or not isinstance(body, dict): - error = 'Missing content or content is not a dict' + if not body or not isinstance(body, (str, bytes)): + error = 'Missing content or content is not string/bytes type' return error @staticmethod diff --git a/requirements.txt b/requirements.txt index e689dca..5b380f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,3 @@ requests-mock==1.3.0 responses==0.5.1 trafaret==1.2.0 unittest2==1.1.0 -flatdict==3.1.0 diff --git a/setup.py b/setup.py index 0e3e8b1..f7ecd03 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def read_version(): author='filestack.com', author_email='support@filestack.com', packages=find_packages(), - install_requires=['requests', 'trafaret', 'future', 'flatdict'], + install_requires=['requests', 'trafaret', 'future'], classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', diff --git a/tests/client_test.py b/tests/client_test.py index b9fbb95..e26bfdb 100644 --- a/tests/client_test.py +++ b/tests/client_test.py @@ -145,41 +145,26 @@ def test_upload_multipart_workflows(post_mock, put_mock, client): def test_webhooks_signature(): - resp = Client.validate_webhook_signature(100, {'test': 'content'}, {'test': 'headers'}) + resp = Client.validate_webhook_signature(100, b'{"test": "content"}', {'test': 'headers'}) assert resp == {'error': 'Missing secret or secret is not a string', 'valid': True} - resp = Client.validate_webhook_signature('a', {'test': 'content'}) + resp = Client.validate_webhook_signature('a', b'{"test": "content"}') assert resp == {'error': 'Missing headers or headers are not a dict', 'valid': True} - resp = Client.validate_webhook_signature('a', '', {'header': 'header'}) - assert resp == {'error': 'Missing content or content is not a dict', 'valid': True} + resp = Client.validate_webhook_signature('a', {"test": "content"}, {'header': 'header'}) + assert resp == {'error': 'Missing content or content is not string/bytes type', 'valid': True} - resp = Client.validate_webhook_signature('a', {'test': 'body'}, {'fs-timestamp': 'header'}) + resp = Client.validate_webhook_signature('a', '{"test": "content"}', {'fs-timestamp': 'header'}) assert resp == {'error': 'Missing `Signature` value in provided headers', 'valid': True} - resp = Client.validate_webhook_signature('a', {'test': 'body'}, {'header': 'header'}) + resp = Client.validate_webhook_signature('a', '{"test": "content"}', {'header': 'header'}) assert resp == {'error': 'Missing `Timestamp` value in provided headers', 'valid': True} - content = { - 'action': 'fp.upload', - 'id': 1000, - 'text': { - 'client': 'Computer', - 'container': 'your-bucket', - 'filename': 'filename.jpg', - 'key': 'kGaeljnga9wkysK6Z_filename.jpg', - 'size': 100000, - 'status': 'Stored', - 'type': 'image/jpeg', - 'url': 'https://cdn.filestackcontent.com/Handle1Handle1Handle1', - 'test': [], - 'test1': {} - }, - 'timestamp': 1558123673 - } + content = '{"timestamp": 1558123673, "id": 1000, "text": {"filename": "filename.jpg", "type": "image/jpeg", "container": "your-bucket", "client": "Computer", "status": "Stored", "url": "https://cdn.filestackcontent.com/Handle1Handle1Handle1", "key": "kGaeljnga9wkysK6Z_filename.jpg", "test": [], "test1": {}, "size": 100000}, "action": "fp.upload"}' secret = 'SecretSecretSecretAA' + headers = { - 'FS-Signature': '4450cd49aad51b689cade0b7d462ae4fdd7e4e5bd972cc3e7fd6373c442871c7', + 'FS-Signature': '4b058e4065206cfc13d57a8480cbd5915f9d2ed4e3fa4179e7fe82c6e58dc6d5', 'FS-Timestamp': '1558384364' } resp = Client.validate_webhook_signature(secret, content, headers) From a960c056c79c7017f01270e9bf3f35607af5bb42 Mon Sep 17 00:00:00 2001 From: Igor Solyony Date: Wed, 29 May 2019 19:09:44 +0200 Subject: [PATCH 2/3] Update readme --- README.md | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index addfc20..3f89e9f 100644 --- a/README.md +++ b/README.md @@ -133,19 +133,8 @@ Verification method give you ability to check if filestack generated webhook whi ```python from filestack import Client -webhook_data = { - "id": 50000, - "action": "fp.upload", - "text": { - "container": "some-bucket", - "url": "https://cdn.filestackcontent.com/Handle", - "filename": "filename.png", - "client": "Computer", - "key": "key_filename.png", - "type": "image/png", - "size": 1000000 - } -} +# webhook_data is raw content received on webhook endpoint +webhook_data = '{"action": "fp.upload", "text": {"container": "some-bucket", "url": "https://cdn.filestackcontent.com/Handle", "filename": "filename.png", "client": "Computer", "key": "key_filename.png", "type": "image/png", "size": 1000000}, "id": 50006}' resp = Client.validate_webhook_signature( '', webhook_data, From 5cf195ef3b110984440afcd5a24367c5418cc52d Mon Sep 17 00:00:00 2001 From: Bartek Kwiecien Date: Thu, 30 May 2019 10:33:22 +0200 Subject: [PATCH 3/3] Changed webhook verification funcion name, updated readme & tests --- README.md | 19 +++++++++---------- filestack/models/filestack_client.py | 2 +- tests/client_test.py | 14 +++++++------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 3f89e9f..14d9744 100644 --- a/README.md +++ b/README.md @@ -128,26 +128,25 @@ new_filelink = client.upload(filepath="path/to/file") ### Webhook verification -Verification method give you ability to check if filestack generated webhook which you received. +You can use `Client.verify_webhook_signature` method to make sure that the webhooks you receive are sent by Filestack. ```python from filestack import Client -# webhook_data is raw content received on webhook endpoint -webhook_data = '{"action": "fp.upload", "text": {"container": "some-bucket", "url": "https://cdn.filestackcontent.com/Handle", "filename": "filename.png", "client": "Computer", "key": "key_filename.png", "type": "image/png", "size": 1000000}, "id": 50006}' +# webhook_data is raw content you receive +webhook_data = b'{"action": "fp.upload", "text": {"container": "some-bucket", "url": "https://cdn.filestackcontent.com/Handle", "filename": "filename.png", "client": "Computer", "key": "key_filename.png", "type": "image/png", "size": 1000000}, "id": 50006}' -resp = Client.validate_webhook_signature( - '', webhook_data, - {'FS-Signature': '', 'FS-Timestamp': ''} +resp = Client.verify_webhook_signature( + '', webhook_data, + {'FS-Signature': '', 'FS-Timestamp': ''} ) if not resp['valid'] and resp['error'] is None: - raise Exception('Webhook signature is invalid') + raise Exception('Webhook signature is invalid') elif resp['error']: - print('Please check input params: {}'.format(resp['error'])) + print('Please check input params: {}'.format(resp['error'])) else: - print('Webhook is valid and was generated by Filestack') - + print('Webhook is valid and was generated by Filestack') ``` ## Versioning diff --git a/filestack/models/filestack_client.py b/filestack/models/filestack_client.py index f9510ce..daedffc 100644 --- a/filestack/models/filestack_client.py +++ b/filestack/models/filestack_client.py @@ -199,7 +199,7 @@ def upload(self, url=None, filepath=None, multipart=True, params=None, upload_pr raise Exception('Invalid API response') @staticmethod - def validate_webhook_signature(secret, body, headers=None): + def verify_webhook_signature(secret, body, headers=None): """ Checks if webhook, which you received was originally from Filestack, based on you secret for webhook endpoint which was generated in Filestack developer portal. diff --git a/tests/client_test.py b/tests/client_test.py index e26bfdb..3ca2c8c 100644 --- a/tests/client_test.py +++ b/tests/client_test.py @@ -145,19 +145,19 @@ def test_upload_multipart_workflows(post_mock, put_mock, client): def test_webhooks_signature(): - resp = Client.validate_webhook_signature(100, b'{"test": "content"}', {'test': 'headers'}) + resp = Client.verify_webhook_signature(100, b'{"test": "content"}', {'test': 'headers'}) assert resp == {'error': 'Missing secret or secret is not a string', 'valid': True} - resp = Client.validate_webhook_signature('a', b'{"test": "content"}') + resp = Client.verify_webhook_signature('a', b'{"test": "content"}') assert resp == {'error': 'Missing headers or headers are not a dict', 'valid': True} - resp = Client.validate_webhook_signature('a', {"test": "content"}, {'header': 'header'}) + resp = Client.verify_webhook_signature('a', {"test": "content"}, {'header': 'header'}) assert resp == {'error': 'Missing content or content is not string/bytes type', 'valid': True} - resp = Client.validate_webhook_signature('a', '{"test": "content"}', {'fs-timestamp': 'header'}) + resp = Client.verify_webhook_signature('a', '{"test": "content"}', {'fs-timestamp': 'header'}) assert resp == {'error': 'Missing `Signature` value in provided headers', 'valid': True} - resp = Client.validate_webhook_signature('a', '{"test": "content"}', {'header': 'header'}) + resp = Client.verify_webhook_signature('a', '{"test": "content"}', {'header': 'header'}) assert resp == {'error': 'Missing `Timestamp` value in provided headers', 'valid': True} content = '{"timestamp": 1558123673, "id": 1000, "text": {"filename": "filename.jpg", "type": "image/jpeg", "container": "your-bucket", "client": "Computer", "status": "Stored", "url": "https://cdn.filestackcontent.com/Handle1Handle1Handle1", "key": "kGaeljnga9wkysK6Z_filename.jpg", "test": [], "test1": {}, "size": 100000}, "action": "fp.upload"}' @@ -167,9 +167,9 @@ def test_webhooks_signature(): 'FS-Signature': '4b058e4065206cfc13d57a8480cbd5915f9d2ed4e3fa4179e7fe82c6e58dc6d5', 'FS-Timestamp': '1558384364' } - resp = Client.validate_webhook_signature(secret, content, headers) + resp = Client.verify_webhook_signature(secret, content, headers) assert resp == {'error': None, 'valid': True} headers['FS-Signature'] = '4450cd49aad51b689cbde0b7d462ae5fdd7e4e5bd972cc3e7fd6373c442871c7' - resp = Client.validate_webhook_signature(secret, content, headers) + resp = Client.verify_webhook_signature(secret, content, headers) assert resp == {'error': None, 'valid': False}