Skip to content

Commit db4f57f

Browse files
committed
first commit
0 parents  commit db4f57f

File tree

5 files changed

+96
-0
lines changed

5 files changed

+96
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.vscode/
2+
tokens.json

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# githubWebhook
2+
3+
A Flask blueprint for receiving GitHub or GitLab webhooks.
4+
5+
## Setup
6+
7+
1. Clone this repo
8+
2. Install packages from requirements.txt
9+
3. Register the webhookBlueprint within a Flask app of your choice
10+
4. Now you just need to setup the webhook on the git side, and you are done
11+
12+
## Default behavior
13+
14+
The default webhookBlueprint requires a git token to be provided for verification.
15+
For GitHub that would be the secret string that you provide [during creation](https://docs.github.com/en/webhooks/using-webhooks/creating-webhooks#creating-a-repository-webhook) and for GitLab that would be the [secret token](https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#validate-payloads-by-using-a-secret-token).
16+
webhookBlueprint creates a POST endpoint / which receives webhook requests, verifies them and passes their JSON content to webhookBlueprint.processWebhook().
17+
webhookBlueprint.processWebhook() by default creates a new git subprocess that performs a git pull.
18+
After the git pull the default method attempts to run an optionally provided unittest.TestSuite.
19+
If these tests fail the method attempts to abort the last merge.
20+
The results of the tests and the abort merge is returned to git.
21+
For security reasons only webhooks with means of verification will be accepted.
22+
23+
## Customization
24+
25+
The class was built with customization in mind.
26+
If you need the processing method to do something else I suggest you simply override the processWebhook method with your own method in the singleton blueprint instance.
27+
Alternatively if you really need to build upon the blueprint, or want a different interface you can always just create a subclass.

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Flask >= 3.0.0

webhook.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from flask import Blueprint, request, Response, abort, Request
2+
from hashlib import sha256
3+
from hmac import new as hmacNew
4+
from subprocess import run
5+
from unittest import TestSuite, TestResult
6+
7+
def verifyGithubSignature(request: Request, token:str) -> bool:
8+
"""Verify the GitHub signature of a webhook request"""
9+
signature = request.headers.get("X-Hub-Signature-256")
10+
if signature is None:
11+
return False
12+
hash_object = hmacNew(token.encode("utf-8"), msg=request.get_data(), digestmod=sha256)
13+
expected_signature = "sha256=" + hash_object.hexdigest()
14+
return signature == expected_signature
15+
16+
class webhookBlueprint(Blueprint):
17+
"""Wrapper over the flask blueprint that creates an endpoint for receiving and processing git webhooks. Overwrite the processWebhook method to process the webhook data."""
18+
def __init__(self, webhookToken:str, tests:TestSuite=None, name:str="webhook", import_name:str=__name__, *args, **kwargs):
19+
super().__init__(name, import_name, *args, **kwargs)
20+
self.webhookToken = webhookToken
21+
self.tests = tests
22+
self.route("/", methods=["POST"])(self.receiveWebhook)
23+
def receiveWebhook(self) -> Response:
24+
"""Receive webhook from GitHub and process it using the processWebhook method."""
25+
if "X-Hub-Signature-256" in request.headers:
26+
if not verifyGithubSignature(request, self.webhookToken):
27+
abort(401)
28+
elif "X-Gitlab-Token":
29+
if request.headers.get("X-Gitlab-Token") != self.token:
30+
abort(401)
31+
else:
32+
abort(400, "Unsupported webhook source")
33+
#at this point the webhook is verified
34+
return self.processWebhook(request.json)
35+
def processWebhook(self, data:dict) -> tuple[int, str]:
36+
"""Process the webhook. Return a tuple of (status code, message)"""
37+
process = run(["/usr/bin/git", "pull"], env=dict(GIT_SSH_COMMAND="/usr/bin/ssh"))
38+
if process.returncode != 0:
39+
return 500, process.stderr.decode("utf-8")
40+
if self.tests is not None:
41+
result:TestResult = self.tests.run()
42+
if result.wasSuccessful():
43+
return 200, "Tests passed"
44+
else:
45+
abortProcess = run(["/usr/bin/git", "merge", "--abort"], env=dict(GIT_SSH_COMMAND="/usr/bin/ssh"))
46+
return 428, f"Tests did not pass, Errors: {result.errors}, Failures: {result.failures}. Merge abort status: {abortProcess.returncode}"
47+
else:
48+
return 200, "Webhook received successfully"
49+
50+
if __name__ == "__main__":
51+
from flask import Flask
52+
app = Flask(__name__)
53+
app.register_blueprint(webhookBlueprint("token"))
54+
app.run()

wsgi.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from webhook import webhookBlueprint
2+
from flask import Flask
3+
import json
4+
5+
app = Flask(__name__)
6+
with open("tokens.json", "r") as f:
7+
token = json.load(f)["webhookGit"]
8+
wb = webhookBlueprint(token, url_prefix="/")
9+
app.register_blueprint(wb)
10+
11+
if __name__ == "__main__":
12+
app.run()

0 commit comments

Comments
 (0)