Skip to content

Commit

Permalink
Merge 5cf195e into 13ba028
Browse files Browse the repository at this point in the history
  • Loading branch information
solyony committed May 30, 2019
2 parents 13ba028 + 5cf195e commit a5c387b
Show file tree
Hide file tree
Showing 5 changed files with 31 additions and 67 deletions.
32 changes: 10 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,37 +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 = {
"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
}
}

resp = Client.validate_webhook_signature(
'<YOUR_WEBHOOK_SECRET>', webhook_data,
{'FS-Signature': '<SIGNATURE-FROM-REQUEST-HEADERS>', 'FS-Timestamp': '<TIMESTAMP-FROM-REQUEST-HEADERS>'}
# 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.verify_webhook_signature(
'<YOUR_WEBHOOK_SECRET>', webhook_data,
{'FS-Signature': '<SIGNATURE-FROM-REQUEST-HEADERS>', 'FS-Timestamp': '<TIMESTAMP-FROM-REQUEST-HEADERS>'}
)

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
Expand Down
26 changes: 9 additions & 17 deletions filestack/models/filestack_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -201,17 +199,18 @@ 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
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'}
)
```
Expand All @@ -229,30 +228,23 @@ 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
if not secret or not isinstance(secret, str):
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
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
37 changes: 11 additions & 26 deletions tests/client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,46 +145,31 @@ 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.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', {'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', '', {'header': 'header'})
assert resp == {'error': 'Missing content or content is not a dict', 'valid': True}
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': 'body'}, {'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': 'body'}, {'header': 'header'})
resp = Client.verify_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)
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}

0 comments on commit a5c387b

Please sign in to comment.