diff --git a/src/octokit/webhook.py b/src/octokit/webhook.py new file mode 100644 index 0000000..a7af8d7 --- /dev/null +++ b/src/octokit/webhook.py @@ -0,0 +1,47 @@ +import hashlib +import hmac +import json +from uuid import UUID + +from octokit import utils + + +def valid_signature(headers, payload, secret): + encoding = 'utf-8' + algo, sig = headers.get('X-Hub-Signature').split('=') + digest = hmac.new(secret.encode(encoding), json.dumps(payload).encode(encoding), getattr(hashlib, algo)).hexdigest() + return hmac.compare_digest(sig.encode(encoding), digest.encode(encoding)) + + +def valid_guid(guid): + try: + return str(UUID(guid)) == guid + except ValueError: + return False + + +def valid_event(event, events): + return event in utils.get_json_data('events.json') and (event in events or '*' in events) + + +def valid_user_agent(ua): + return ua.startswith('GitHub-Hookshot/') + + +def valid_headers(headers, events, verify_user_agent): + if not valid_guid(headers.get('X-GitHub-Delivery')): + return False + if not valid_event(headers.get('X-GitHub-Event'), events): + return False + if verify_user_agent and not valid_user_agent(headers.get('User-Agent', '')): + return False + return True + + +def verify(headers, payload, secret, events=[], verify_user_agent=False, return_app_id=False): + if not valid_headers(headers, events, verify_user_agent): + return False + validity = valid_signature(headers, payload, secret) + if validity and return_app_id and headers.get('X-GitHub-Event') == 'ping': + return payload.get('hook').get('app_id') + return validity diff --git a/src/octokit/webhook/__init__.py b/src/octokit/webhook/__init__.py deleted file mode 100644 index f0ba4c6..0000000 --- a/src/octokit/webhook/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -import hashlib -import hmac -import json -from uuid import UUID - -from octokit import utils - - -def _compare(headers, payload, secret): - encoding = 'utf-8' - algo, sig = headers.get('X-Hub-Signature').split('=') - digest = hmac.new(secret.encode(encoding), json.dumps(payload).encode(encoding), getattr(hashlib, algo)).hexdigest() - return hmac.compare_digest(sig.encode(encoding), digest.encode(encoding)) - - -def invalid_guid(guid): - try: - return str(UUID(guid)) != guid - except ValueError: - return True - - -def invalid_event(event, events): - if event not in utils.get_json_data('events.json'): - return True - if event not in events and '*' not in events: - return True - - -def valid_user_agent(ua): - return ua.startswith('GitHub-Hookshot/') - - -def verify(headers, payload, secret, events=[], verify_user_agent=False): - if invalid_guid(headers.get('X-GitHub-Delivery')): - return False - if invalid_event(headers.get('X-GitHub-Event'), events): - return False - if verify_user_agent and not valid_user_agent(headers.get('User-Agent', '')): - return False - return _compare(headers, payload, secret) diff --git a/tests/test_webhook.py b/tests/test_webhook.py index fdc3dfc..8c30d20 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -89,3 +89,33 @@ class TestWebhook(object): secret = 'secret' events = ['push'] assert webhook.verify(headers, payload, secret, events=events, verify_user_agent=True) is False + + def test_verify_ping_event(self): + headers = { + 'X-Hub-Signature': 'sha1=02ad50e35622a4cf597de5fb86f4d59520f5adda', + 'X-GitHub-Event': 'ping', + 'X-GitHub-Delivery': '72d3162f-cc78-11e3-81ab-4c9367dc0958', + 'User-Agent': 'GitHub-Hookshot/', + } + payload = { + 'hook': { + 'type': 'App', + 'id': 11, + 'active': True, + 'events': ['pull_request'], + 'app_id': 42, + } + } + secret = 'secret' + assert webhook.verify(headers, payload, secret, events=['*'], return_app_id=True) == 42 + + def test_can_request_app_id_be_returned_on_non_ping_events(self): + headers = { + 'X-Hub-Signature': 'sha1=5d61605c3feea9799210ddcb71307d4ba264225f', + 'X-GitHub-Event': 'push', + 'X-GitHub-Delivery': '72d3162f-cc78-11e3-81ab-4c9367dc0958', + 'User-Agent': 'GitHub-Hookshot/', + } + payload = {} + secret = 'secret' + assert webhook.verify(headers, payload, secret, events=['*'], return_app_id=True)