Skip to content

Commit

Permalink
Merge branch 'release/2.6.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Bartek Kwiecien committed May 29, 2019
2 parents 2e2251e + a56ad80 commit b989b5b
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 65 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ temp_files/
test.zip
tests/data/test_download.jpg
.coverage
.idea
.venv
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Filestack-Python Changelog

### 2.6.0 (May 29th, 2019)
- Added webhook signature verification

### 2.5.0 (May 20th, 2019)
- Added support for [Filestack Workflows](https://www.filestack.com/products/workflows/)

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.5.0
2.6.0
2 changes: 1 addition & 1 deletion filestack/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '2.5.0'
__version__ = '2.6.0'
from .models.filestack_client import Client
from .models.filestack_filelink import Filelink
from .models.filestack_security import security
Expand Down
78 changes: 73 additions & 5 deletions filestack/models/filestack_client.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import mimetypes
import os
import re
import hmac
import json
import hashlib
import requests
import mimetypes

from flatdict import FlatterDict

import filestack.models

from filestack.config import API_URL, CDN_URL, STORE_PATH, HEADERS
from filestack.trafarets import STORE_LOCATION_SCHEMA, STORE_SCHEMA
from filestack.utils import utils
from filestack.utils import upload_utils
from filestack.utils import intelligent_ingestion
from filestack.utils import utils, upload_utils, intelligent_ingestion


class Client():
class Client:
"""
The hub for all Filestack operations. Creates Filelinks, converts external to transform objects,
takes a URL screenshot and returns zipped files.
Expand Down Expand Up @@ -197,6 +200,71 @@ def upload(self, url=None, filepath=None, multipart=True, params=None, upload_pr
else:
raise Exception('Invalid API response')

@staticmethod
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
returns [Dict]
```python
from filestack import Client
result = client.validate_webhook_signature(
'secret', {'webhook_content': 'received_from_filestack'},
{'FS-Timestamp': '1558367878', 'FS-Signature': 'Filestack Signature'}
)
```
Response will contain keys 'error' and 'valid'.
If 'error' is not None - it means that you provided wrong parameters
If 'valid' is False - it means that signature is invalid and probably Filestack is not source of webhook
"""
error = Client.validate_webhook_params(secret, body, headers)

if error:
return {'error': error, 'valid': True}

error, headers_prepared = Client.prepare_and_validate_webhook_headers(headers)

if error:
return {'error': error, 'valid': True}

cleaned_dict = Client.cleanup_webhook_dict(body)

sign = "%s.%s" % (headers_prepared['fs-timestamp'], json.dumps(cleaned_dict, sort_keys=True))
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'
return error

@staticmethod
def prepare_and_validate_webhook_headers(headers):
error = None
headers_prepared = dict((k.lower(), v) for k, v in headers.items())
if 'fs-signature' not in headers_prepared:
error = 'Missing `Signature` value in provided headers'
if 'fs-timestamp' not in headers_prepared:
error = 'Missing `Timestamp` value in provided headers'
return error, headers_prepared

@property
def security(self):
"""
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ requests-mock==1.3.0
responses==0.5.1
trafaret==1.2.0
unittest2==1.1.0
flatdict==3.1.0
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ def read_version():
version=read_version(),
license='Apache 2.0',
description='Filestack REST API Library',
long_description=read('README.md'),
long_description='Visit: https://github.com/filestack/filestack-python',
url='https://github.com/filestack/filestack-python',
author='filestack.com',
author_email='support@filestack.com',
packages=find_packages(),
install_requires=['requests', 'trafaret', 'future'],
install_requires=['requests', 'trafaret', 'future', 'flatdict'],
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
Expand Down
111 changes: 55 additions & 56 deletions tests/client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
HANDLE = 'SOMEHANDLE'


class MockResponse():
class MockResponse:
ok = True
headers = {'ETag': 'some_tag'}

Expand Down Expand Up @@ -97,61 +97,14 @@ def api_zip(url, request):


@pytest.mark.parametrize('store_params, expected_url_part', [
[
{
'filename': 'image.jpg'
},
'filename:image.jpg'
],
[
{
'location': 'S3'
},
'location:S3'
],
[
{
'path': 'some_path'
},
'path:some_path'
],
[
{
'container': 'container_id'
},
'container:container_id'
],
[
{
'region': 'us-east-1'
},
'region:us-east-1'
],
[
{
'access': 'public'
},
'access:public'
],
[
{
'base64decode': True
},
'base64decode:True'
],
[
{
'workflows': ['workflows_id_1']
},
'workflows:[%22workflows_id_1%22]'
]
[{'filename': 'image.jpg'}, 'filename:image.jpg'],
[{'location': 'S3'}, 'location:S3'],
[{'path': 'some_path'}, 'path:some_path'],
[{'container': 'container_id'}, 'container:container_id'],
[{'region': 'us-east-1'}, 'region:us-east-1'],
[{'access': 'public'}, 'access:public'],
[{'base64decode': True}, 'base64decode:True'],
[{'workflows': ['workflows_id_1']}, 'workflows:[%22workflows_id_1%22]']
])
def test_url_store_task(store_params, expected_url_part, client):
@urlmatch(netloc=r'cdn.filestackcontent\.com', method='post', scheme='https')
Expand Down Expand Up @@ -189,3 +142,49 @@ def test_upload_multipart_workflows(post_mock, put_mock, client):

assert 'workflows' in post_mock.call_args[1]['data'].keys() and post_mock.call_args[1]['data']['workflows'] == expected_request_data['workflows']
assert new_filelink.handle == 'new_handle'


def test_webhooks_signature():
resp = Client.validate_webhook_signature(100, {'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'})
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': 'body'}, {'fs-timestamp': 'header'})
assert resp == {'error': 'Missing `Signature` value in provided headers', 'valid': True}

resp = Client.validate_webhook_signature('a', {'test': 'body'}, {'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
}
secret = 'SecretSecretSecretAA'
headers = {
'FS-Signature': '4450cd49aad51b689cade0b7d462ae4fdd7e4e5bd972cc3e7fd6373c442871c7',
'FS-Timestamp': '1558384364'
}
resp = Client.validate_webhook_signature(secret, content, headers)
assert resp == {'error': None, 'valid': True}

headers['FS-Signature'] = '4450cd49aad51b689cbde0b7d462ae5fdd7e4e5bd972cc3e7fd6373c442871c7'
resp = Client.validate_webhook_signature(secret, content, headers)
assert resp == {'error': None, 'valid': False}

0 comments on commit b989b5b

Please sign in to comment.