From 8d3ad248126c15195d0f21b436ca05b3718bc255 Mon Sep 17 00:00:00 2001 From: Shibani Mahapatra Date: Tue, 31 Dec 2019 09:00:59 +0000 Subject: [PATCH 1/5] Add support for 'application/x-www-form-urlencoded' Add support for Content-Type: application/x-www-form-urlencoded, so that `python-github-webhook` supports both Content-Types supported by GitHub webhooks. Signed-Off-By: Shibani Mahapatra --- github_webhook/webhook.py | 9 +++++++-- setup.py | 2 +- tests/test_webhook.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/github_webhook/webhook.py b/github_webhook/webhook.py index d6addf9..85787d3 100644 --- a/github_webhook/webhook.py +++ b/github_webhook/webhook.py @@ -2,7 +2,7 @@ import hashlib import hmac import logging - +import json import six from flask import abort, request @@ -58,7 +58,12 @@ def _postreceive(self): abort(400, "Invalid signature") event_type = _get_header("X-Github-Event") - data = request.get_json() + content_type = _get_header("content-type") + data = ( + json.loads(request.form.to_dict(flat=True)["payload"]) + if content_type == "application/x-www-form-urlencoded" + else request.get_json() + ) if data is None: abort(400, "Request body must contain json") diff --git a/setup.py b/setup.py index cea1aa4..ebd03dc 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="github-webhook", - version="1.0.2", + version="1.0.3", description="Very simple, but powerful, microframework for writing Github webhooks in Python", url="https://github.com/bloomberg/python-github-webhook", author="Alex Chamberlain, Fred Phillips, Daniel Kiss, Daniel Beer", diff --git a/tests/test_webhook.py b/tests/test_webhook.py index 6575632..b6e64a2 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -4,6 +4,7 @@ import pytest import werkzeug +import json try: from unittest import mock @@ -23,6 +24,14 @@ def mock_request(): @pytest.fixture def push_request(mock_request): mock_request.headers["X-Github-Event"] = "push" + mock_request.headers["content-type"] = "application/json" + yield mock_request + + +@pytest.fixture +def push_request_encoded(mock_request): + mock_request.headers["X-Github-Event"] = "push" + mock_request.headers["content-type"] = "application/x-www-form-urlencoded" yield mock_request @@ -64,9 +73,35 @@ def test_run_push_hook(webhook, handler, push_request): handler.assert_called_once_with(push_request.get_json.return_value) +def test_run_push_hook_urlencoded(webhook, handler, push_request_encoded): + github_mock_payload = {"payload": '{"key": "value"}'} + push_request_encoded.form.to_dict.return_value = github_mock_payload + payload = json.loads(github_mock_payload["payload"]) + + # WHEN + webhook._postreceive() + + # THEN + handler.assert_called_once_with(payload) + + def test_do_not_run_push_hook_on_ping(webhook, handler, mock_request): # GIVEN mock_request.headers["X-Github-Event"] = "ping" + mock_request.headers["content-type"] = "application/json" + + # WHEN + webhook._postreceive() + + # THEN + handler.assert_not_called() + + +def test_do_not_run_push_hook_on_ping_urlencoded(webhook, handler, mock_request): + # GIVEN + mock_request.headers["X-Github-Event"] = "ping" + mock_request.headers["content-type"] = "application/x-www-form-urlencoded" + mock_request.form.to_dict.return_value = {"payload": '{"key": "value"}'} # WHEN webhook._postreceive() From 2a1e1fbbb74c66761079778ddf48e5c66ee7be50 Mon Sep 17 00:00:00 2001 From: Sergio Isidoro Date: Sun, 26 Jan 2020 11:19:33 +0200 Subject: [PATCH 2/5] Support for init_app Fixes: #24 --- github_webhook/webhook.py | 13 +++++++++++-- tests/test_webhook.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/github_webhook/webhook.py b/github_webhook/webhook.py index 85787d3..0a8b6c2 100644 --- a/github_webhook/webhook.py +++ b/github_webhook/webhook.py @@ -16,11 +16,20 @@ class Webhook(object): :param secret: Optional secret, used to authenticate the hook comes from Github """ - def __init__(self, app, endpoint="/postreceive", secret=None): - app.add_url_rule(rule=endpoint, endpoint=endpoint, view_func=self._postreceive, methods=["POST"]) + def __init__(self, app=None, endpoint="/postreceive", secret=None): + self.app = app + self.set_secret(secret) + if app is not None: + self.init_app(app, endpoint, secret) + def init_app(self, app, endpoint="/postreceive", secret=None): self._hooks = collections.defaultdict(list) self._logger = logging.getLogger("webhook") + if secret is not None: + self.set_secret(secret) + app.add_url_rule(rule=endpoint, endpoint=endpoint, view_func=self._postreceive, methods=["POST"]) + + def set_secret(self, secret=None): if secret is not None and not isinstance(secret, six.binary_type): secret = secret.encode("utf-8") self._secret = secret diff --git a/tests/test_webhook.py b/tests/test_webhook.py index b6e64a2..b6527b5 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -65,6 +65,44 @@ def test_constructor(): ) +def test_init_app_flow(): + # GIVEN + app = mock.Mock() + + # WHEN + webhook = Webhook() + webhook.init_app(app) + + # THEN + app.add_url_rule.assert_called_once_with( + endpoint="/postreceive", rule="/postreceive", view_func=webhook._postreceive, methods=["POST"] + ) + + +def test_init_app_flow_should_not_accidentally_override_secrets(): + # GIVEN + app = mock.Mock() + + # WHEN + webhook = Webhook(secret="hello-world-of-secrecy") + webhook.init_app(app) + + # THEN + assert webhook._secret is not None + + +def test_init_app_flow_should_override_secrets(): + # GIVEN + app = mock.Mock() + + # WHEN + webhook = Webhook(secret="hello-world-of-secrecy") + webhook.init_app(app, secret="a-new-world-of-secrecy") + + # THEN + assert webhook._secret == "a-new-world-of-secrecy".encode("utf-8") + + def test_run_push_hook(webhook, handler, push_request): # WHEN webhook._postreceive() From ddd33ac743dbac35222057f95cafa91bff78652a Mon Sep 17 00:00:00 2001 From: Alex Chamberlain Date: Fri, 28 Feb 2020 09:18:40 +0000 Subject: [PATCH 3/5] Use property instead of setter Signed-off-by: Alex Chamberlain --- github_webhook/webhook.py | 11 ++++++++--- tests/test_webhook.py | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/github_webhook/webhook.py b/github_webhook/webhook.py index 0a8b6c2..f4ca352 100644 --- a/github_webhook/webhook.py +++ b/github_webhook/webhook.py @@ -18,7 +18,7 @@ class Webhook(object): def __init__(self, app=None, endpoint="/postreceive", secret=None): self.app = app - self.set_secret(secret) + self.secret = secret if app is not None: self.init_app(app, endpoint, secret) @@ -26,10 +26,15 @@ def init_app(self, app, endpoint="/postreceive", secret=None): self._hooks = collections.defaultdict(list) self._logger = logging.getLogger("webhook") if secret is not None: - self.set_secret(secret) + self.secret = secret app.add_url_rule(rule=endpoint, endpoint=endpoint, view_func=self._postreceive, methods=["POST"]) - def set_secret(self, secret=None): + @property + def secret(self): + return self._secret + + @secret.setter + def secret(self, secret): if secret is not None and not isinstance(secret, six.binary_type): secret = secret.encode("utf-8") self._secret = secret diff --git a/tests/test_webhook.py b/tests/test_webhook.py index b6527b5..29eba7d 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -88,7 +88,7 @@ def test_init_app_flow_should_not_accidentally_override_secrets(): webhook.init_app(app) # THEN - assert webhook._secret is not None + assert webhook.secret is not None def test_init_app_flow_should_override_secrets(): @@ -100,7 +100,7 @@ def test_init_app_flow_should_override_secrets(): webhook.init_app(app, secret="a-new-world-of-secrecy") # THEN - assert webhook._secret == "a-new-world-of-secrecy".encode("utf-8") + assert webhook.secret == "a-new-world-of-secrecy".encode("utf-8") def test_run_push_hook(webhook, handler, push_request): From 6a24c6f1589a14a780378937f126282d32b32a35 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 3 Sep 2019 20:29:08 -0400 Subject: [PATCH 4/5] Add .gitignore file As part of the normal process of developing python-github-webhook local artifacts are generated from running python code, running tox, and running tests. These are not things that should be committed to the repo. To avoid accidents or mistakes when a file that shouldn't ever be added to the repo is included in a commit this commit adds a .gitignore file and configures it to not include any of these artifacts of normal development. This way including a file like that by mistake is not likely to ever happen. Signed-off-by: Matthew Treinish --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ee8c50 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +.tox/* +*.pyc +.coverage* +!.coveragerc +*egg* From 61e713c3781e2de6e327554be54095df2d666604 Mon Sep 17 00:00:00 2001 From: Alex Chamberlain Date: Fri, 6 Mar 2020 11:18:46 +0000 Subject: [PATCH 5/5] Release version 1.0.4 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ebd03dc..b1ebbae 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="github-webhook", - version="1.0.3", + version="1.0.4", description="Very simple, but powerful, microframework for writing Github webhooks in Python", url="https://github.com/bloomberg/python-github-webhook", author="Alex Chamberlain, Fred Phillips, Daniel Kiss, Daniel Beer",