164 lines
6.3 KiB
Python
164 lines
6.3 KiB
Python
# Source: https://github.com/bloomberg/python-github-webhook
|
|
# License: Apache 2.0
|
|
# Modification: Changed `request.data` to `request.get_data()` in `_get_digest()`.
|
|
# Issue: https://github.com/bloomberg/python-github-webhook/issues/29
|
|
|
|
import collections
|
|
import hashlib
|
|
import hmac
|
|
import logging
|
|
import json
|
|
import six
|
|
from flask import abort, request
|
|
|
|
|
|
class Webhook(object):
|
|
"""
|
|
Construct a webhook on the given :code:`app`.
|
|
|
|
:param app: Flask app that will host the webhook
|
|
:param endpoint: the endpoint for the registered URL rule
|
|
:param secret: Optional secret, used to authenticate the hook comes from Github
|
|
"""
|
|
|
|
def __init__(self, app=None, endpoint="/postreceive", secret=None):
|
|
self.app = app
|
|
self.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.secret = secret
|
|
app.add_url_rule(rule=endpoint, endpoint=endpoint, view_func=self._postreceive, methods=["POST"])
|
|
|
|
@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
|
|
|
|
def hook(self, event_type="push"):
|
|
"""
|
|
Registers a function as a hook. Multiple hooks can be registered for a given type, but the
|
|
order in which they are invoke is unspecified.
|
|
|
|
:param event_type: The event type this hook will be invoked for.
|
|
"""
|
|
|
|
def decorator(func):
|
|
self._hooks[event_type].append(func)
|
|
return func
|
|
|
|
return decorator
|
|
|
|
def _get_digest(self):
|
|
"""Return message digest if a secret key was provided"""
|
|
|
|
return hmac.new(self._secret, request.get_data(), hashlib.sha1).hexdigest() if self._secret else None
|
|
|
|
def _postreceive(self):
|
|
"""Callback from Flask"""
|
|
|
|
digest = self._get_digest()
|
|
|
|
if digest is not None:
|
|
sig_parts = _get_header("X-Hub-Signature").split("=", 1)
|
|
if not isinstance(digest, six.text_type):
|
|
digest = six.text_type(digest)
|
|
|
|
if len(sig_parts) < 2 or sig_parts[0] != "sha1" or not hmac.compare_digest(sig_parts[1], digest):
|
|
abort(400, "Invalid signature")
|
|
|
|
event_type = _get_header("X-Github-Event")
|
|
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")
|
|
|
|
self._logger.info("%s (%s)", _format_event(event_type, data), _get_header("X-Github-Delivery"))
|
|
|
|
for hook in self._hooks.get(event_type, []):
|
|
hook(data)
|
|
|
|
return "", 204
|
|
|
|
|
|
def _get_header(key):
|
|
"""Return message header"""
|
|
|
|
try:
|
|
return request.headers[key]
|
|
except KeyError:
|
|
abort(400, "Missing header: " + key)
|
|
|
|
|
|
EVENT_DESCRIPTIONS = {
|
|
"commit_comment": "{comment[user][login]} commented on " "{comment[commit_id]} in {repository[full_name]}",
|
|
"create": "{sender[login]} created {ref_type} ({ref}) in " "{repository[full_name]}",
|
|
"delete": "{sender[login]} deleted {ref_type} ({ref}) in " "{repository[full_name]}",
|
|
"deployment": "{sender[login]} deployed {deployment[ref]} to "
|
|
"{deployment[environment]} in {repository[full_name]}",
|
|
"deployment_status": "deployment of {deployement[ref]} to "
|
|
"{deployment[environment]} "
|
|
"{deployment_status[state]} in "
|
|
"{repository[full_name]}",
|
|
"fork": "{forkee[owner][login]} forked {forkee[name]}",
|
|
"gollum": "{sender[login]} edited wiki pages in {repository[full_name]}",
|
|
"issue_comment": "{sender[login]} commented on issue #{issue[number]} " "in {repository[full_name]}",
|
|
"issues": "{sender[login]} {action} issue #{issue[number]} in " "{repository[full_name]}",
|
|
"member": "{sender[login]} {action} member {member[login]} in " "{repository[full_name]}",
|
|
"membership": "{sender[login]} {action} member {member[login]} to team " "{team[name]} in {repository[full_name]}",
|
|
"page_build": "{sender[login]} built pages in {repository[full_name]}",
|
|
"ping": "ping from {sender[login]}",
|
|
"public": "{sender[login]} publicized {repository[full_name]}",
|
|
"pull_request": "{sender[login]} {action} pull #{pull_request[number]} in " "{repository[full_name]}",
|
|
"pull_request_review": "{sender[login]} {action} {review[state]} "
|
|
"review on pull #{pull_request[number]} in "
|
|
"{repository[full_name]}",
|
|
"pull_request_review_comment": "{comment[user][login]} {action} comment "
|
|
"on pull #{pull_request[number]} in "
|
|
"{repository[full_name]}",
|
|
"push": "{pusher[name]} pushed {ref} in {repository[full_name]}",
|
|
"release": "{release[author][login]} {action} {release[tag_name]} in " "{repository[full_name]}",
|
|
"repository": "{sender[login]} {action} repository " "{repository[full_name]}",
|
|
"status": "{sender[login]} set {sha} status to {state} in " "{repository[full_name]}",
|
|
"team_add": "{sender[login]} added repository {repository[full_name]} to " "team {team[name]}",
|
|
"watch": "{sender[login]} {action} watch in repository " "{repository[full_name]}",
|
|
}
|
|
|
|
|
|
def _format_event(event_type, data):
|
|
try:
|
|
return EVENT_DESCRIPTIONS[event_type].format(**data)
|
|
except KeyError:
|
|
return event_type
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Copyright 2015 Bloomberg Finance L.P.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ----------------------------- END-OF-FILE -----------------------------------
|