Skip to content

Commit

Permalink
Merge pull request #6 from noahhai/auth
Browse files Browse the repository at this point in the history
Auth
  • Loading branch information
noahhai authored Feb 15, 2018
2 parents 36af06a + c1c4135 commit 16323db
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 51 deletions.
1 change: 1 addition & 0 deletions .cache/v/cache/lastfailed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ output/*.html
output/*/index.html

# Sphinx
docs/_build
docs/_build

# idea
.idea/*
4 changes: 3 additions & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ standardjson>=0.3.1
requests>=2.2.1
rq>=0.4.5
wrapt>=1.8.0
pycrypto>=2.6.1

# Webhook test dependencies
coverage
pytest
pytest-cov
flask
flask
ddt
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
cached-property>=0.1.2
standardjson>=0.3.1
requests>=2.2.1
wrapt>=1.8.0
rq>=0.4.5
wrapt>=1.8.0
pycrypto>=2.6.1
urllib
5 changes: 3 additions & 2 deletions tests/test_base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from subprocess import Popen
from test_server import app
from tests.test_server import app
import logging
import sys
import os


def start_test_server():
app.user_reloader = False
test_server = Popen(["python", "test_server.py"])
test_server = Popen(["python", os.path.join(os.path.dirname(__file__), "test_server.py")])
return test_server

def end_test_server(test_server):
Expand Down
13 changes: 0 additions & 13 deletions tests/test_senders_async_redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,12 @@
# -*- coding: utf-8 -*-

import unittest
from ddt import data, ddt
import test_base
from redis import Redis

from webhooks import webhook
from webhooks.senders import async_redis

@ddt
class SendersAsyncRedisCase(unittest.TestCase):

@classmethod
def setUpClass(cls):
cls.test_server = test_base.start_test_server()
test_base.configure_debug_logging()

@classmethod
def tearDownClass(cls):
test_base.end_test_server(cls.test_server)

def test_redis_sender(self):

redis_connection = Redis()
Expand Down
43 changes: 39 additions & 4 deletions tests/test_senders_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@

from webhooks import webhook, unhashed_hook
from webhooks.senders.simple import sender
import json
from standardjson import StandardJSONEncoder
from Crypto.Hash import SHA256


def test_simple_hashed():

@webhook(event="example200", sender_callable=sender)
def basic(wife, husband, creator):
def basic(wife, husband, creator, encoding, url):
return {"husband": husband, "wife": wife}

status = basic("Audrey Roy Greenfeld", "Daniel Roy Greenfeld", creator="pydanny")
status = basic("Audrey Roy Greenfeld", "Daniel Roy Greenfeld", creator="pydanny", encoding="application/json", url="http://httpbin.org")

assert status['wife'] == "Audrey Roy Greenfeld"
assert status['husband'] == "Daniel Roy Greenfeld"
Expand All @@ -29,11 +32,43 @@ def basic(wife, husband, creator):
def test_simple_unhash():

@unhashed_hook(event="example200", sender_callable=sender)
def basic(wife, husband, creator):
def basic(wife, husband, creator, encoding, url):
return {"husband": husband, "wife": wife}

status = basic("Audrey Roy Greenfeld", "Daniel Roy Greenfeld", creator="pydanny")
status = basic("Audrey Roy Greenfeld", "Daniel Roy Greenfeld", creator="pydanny", encoding="application/json", url="http://httpbin.org")

assert status['wife'] == "Audrey Roy Greenfeld"
assert status['husband'] == "Daniel Roy Greenfeld"
assert "hash" not in status


def test_simple_custom_header():

@unhashed_hook(event="example200", sender_callable=sender)
def basic(wife, husband, creator, encoding, url, custom_headers):
return {"husband": husband, "wife": wife}

status = basic("Audrey Roy Greenfeld", "Daniel Roy Greenfeld", creator="pydanny", encoding="application/json", custom_headers = {"Basic" : "dXNlcjpzdXBlcnNlY3JldA=="}, url="http://httpbin.org")

assert status['wife'] == "Audrey Roy Greenfeld"
assert status['husband'] == "Daniel Roy Greenfeld"
assert status['post_attributes']['headers']['Basic'] == "dXNlcjpzdXBlcnNlY3JldA=="



def test_simple_signature():

@unhashed_hook(event="example200", sender_callable=sender)
def basic(wife, husband, creator, encoding, url, signing_secret):
return {"husband": husband, "wife": wife}

status = basic("Audrey Roy Greenfeld", "Daniel Roy Greenfeld", creator="pydanny", encoding="application/json", url="http://httpbin.org", signing_secret = "secret_key")

assert status['wife'] == "Audrey Roy Greenfeld"
assert status['husband'] == "Daniel Roy Greenfeld"
signature = status['post_attributes']['headers']['x-hub-signature']
body = {"wife": "Audrey Roy Greenfeld", "husband": "Daniel Roy Greenfeld"}
hash = SHA256.new("secret_key")
hash.update(json.dumps(body, cls=StandardJSONEncoder))
expected_signature = "sha256="+hash.hexdigest()
assert signature == expected_signature
16 changes: 10 additions & 6 deletions tests/test_senders_targeted.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pytest
import unittest
from webhooks import webhook
from webhooks.senders import targeted
import test_base
from tests import test_base
import json
from ddt import ddt, data

@ddt
class SendersTargetedCase(unittest.TestCase):

@classmethod
def setUpClass(cls):
cls.test_server = test_base.start_test_server()
def setup_class(cls):
test_base.configure_debug_logging()
cls.test_server = test_base.start_test_server()

@classmethod
def tearDownClass(cls):
test_base.end_test_server(cls.test_server)
def setup_class(cls):
#test_base.end_test_server(cls.test_server)
pass

@data("application/x-www-form-urlencoded", "application/json")
def test_encoding(self, encoding):
Expand All @@ -35,7 +37,9 @@ def basic(wife, husband, url, encoding):

@data({'timeout': 10, 'expected_success': True}, {'timeout': 0.010, 'expected_success': False})
def test_timeout(self, params):

from subprocess import Popen
import os
test_server = Popen(["python", os.path.join(os.path.dirname(__file__), "test_server.py")])
@webhook(sender_callable=targeted.sender)
def basic(wife, husband, url, encoding, timeout):
return {"husband": husband, "wife": wife}
Expand Down
4 changes: 3 additions & 1 deletion tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,16 @@ def webhook():

elif request.method == 'POST':
client = request.remote_addr
print('authentication:')
print(request.headers.get('Basic',None))
if not check_auth or client in authorized_clients:
if check_auth and datetime.now() - authorized_clients.get(client) > timedelta(hours=CLIENT_AUTH_TIMEOUT):
authorized_clients.pop(client)
return jsonify({'status':'authorisation timeout'}), 401
else:
print(request.data)
return jsonify({'status':'success' \
, 'received_data: ' : request.data
, 'received_data: ' : request.data.decode('utf-8')
}), 200
else:
return jsonify({'status':'not authorised'}), 401
Expand Down
2 changes: 1 addition & 1 deletion webhooks/senders/async_redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def sender(wrapped, dkwargs, hash_value=None, *args, **kwargs):

senderobj.url = value_in("url", dkwargs, kwargs)
connection = value_in("connection", dkwargs, kwargs)
redis_timeout = value_in_opt("redis_timeout", False, dkwargs, kwargs)
redis_timeout = value_in_opt("redis_timeout", dkwargs, kwargs)

@job('default', connection=connection, timeout=redis_timeout)
def worker(senderobj):
Expand Down
75 changes: 54 additions & 21 deletions webhooks/senders/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import logging
import sys
from time import sleep
from Crypto.Hash import SHA256
from urllib import urlencode

from cached_property import cached_property
from standardjson import StandardJSONEncoder
Expand All @@ -27,7 +29,6 @@ def __init__(self, wrapped, dkwargs, hash_value, attempts, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.attempt = 0
self.success = False
self.error = None
self.response = None

Expand All @@ -40,7 +41,21 @@ def url(self):
return self.get_url()

def get_url(self):
return "http://httpbin.org/post"
return _value_in('url', True, kwargs=self.kwargs, dkwargs=self.dkwargs)

@cached_property
def custom_headers(self):
return self.get_custom_headers()

def get_custom_headers(self):
return _value_in('custom_headers', False, kwargs=self.kwargs, dkwargs=self.dkwargs)

@cached_property
def signing_secret(self):
return self.get_signing_secret()

def get_signing_secret(self):
return _value_in('signing_secret', False, kwargs=self.kwargs, dkwargs=self.dkwargs)

@cached_property
def encoding(self):
Expand Down Expand Up @@ -105,20 +120,28 @@ def _send(self):
""" Send the webhook method """

payload = self.payload
payload['url'] = self.url
sending_metadata = {'success': False}
post_attributes = {'timeout': self.timeout}

if self.custom_headers:
post_attributes['headers'] = self.custom_headers

encoding_key = 'json' if self.encoding == EncodingType.JSON else 'data'
post_attributes[encoding_key] = self.format_payload()

if self.signing_secret:
if not post_attributes.get('headers', None):
post_attributes['headers'] = {}
post_attributes['headers']['x-hub-signature'] = self.create_signature(post_attributes[encoding_key], \
self.signing_secret)

for i, wait in enumerate(range(len(self.attempts) - 1)):

self.attempt = i + 1
sending_metadata['attempt'] = self.attempt

payload['attempt'] = self.attempt

# post the payload
skip_response = False
post_attributes = {'timeout' : self.timeout}
encoding_key = 'json' if self.encoding == EncodingType.JSON else 'data'
post_attributes[encoding_key] = self.format_payload()
try:
print(self.url)
self.response = requests.post(self.url, **post_attributes)

if sys.version > '3':
Expand All @@ -127,7 +150,7 @@ def _send(self):
else:
self.response_content = self.response.content

payload['status_code'] = self.response.status_code
sending_metadata['status_code'] = self.response.status_code

# anything with a 200 status code is a success
if self.response.status_code >= 200 and self.response.status_code < 300:
Expand All @@ -136,15 +159,15 @@ def _send(self):
self.notify("Attempt {}: Successfully sent webhook {}".format(
self.attempt, self.hash_value)
)
payload['response'] = self.response_content
self.success = True
sending_metadata['response'] = self.response_content
sending_metadata['success'] = True
break
else:
self.error = "Status code {}".format(self.response.status_code)

except Exception as ex:
err_formatted = str(ex).replace('"',"'")
payload['response'] = '{"status_code": 500, "status":"failure","error":"'+err_formatted+'"}'
sending_metadata['response'] = '{"status_code": 500, "status":"failure","error":"'+err_formatted+'"}'
self.error = err_formatted

self.notify("Attempt {}: Could not send webhook {}".format(
Expand All @@ -163,9 +186,19 @@ def _send(self):
# Wait a bit before the next attempt
sleep(wait)

payload['success'] = self.success
payload['error'] = None if self.success or not self.error else self.error
return payload
sending_metadata['error'] = None if sending_metadata['success'] or not self.error else self.error
sending_metadata['post_attributes'] = post_attributes
merged_dict = sending_metadata.copy()
merged_dict.update(payload)
return merged_dict

def create_signature(self, payload, secret):
if not isinstance(payload,basestring):
# Data will be forms encoded
payload = requests.PreparedRequest()._encode_params(payload)
hmac = SHA256.new(secret)
hmac.update(payload)
return 'sha256=' + hmac.hexdigest()

def _value_in(key, required, dkwargs, kwargs):
if key in kwargs:
Expand All @@ -180,10 +213,10 @@ def _value_in(key, required, dkwargs, kwargs):
def value_in(key, dkwargs, kwargs):
return _value_in(key, True, dkwargs, kwargs)

def value_in_opt(key, required, dkwargs, kwargs):
return _value_in(key, required, dkwargs, kwargs)
def value_in_opt(key, dkwargs, kwargs):
return _value_in(key, False, dkwargs, kwargs)


class EncodingType(object):
JSON = 'application/x-www-form-urlencoded'
FORMS = 'application/json'
FORMS = 'application/x-www-form-urlencoded'
JSON = 'application/json'

0 comments on commit 16323db

Please sign in to comment.