Skip to content

Commit 5b0be2e

Browse files
First code drop with basic untested skeleton.
1 parent 8316490 commit 5b0be2e

4 files changed

Lines changed: 157 additions & 0 deletions

File tree

README.rst

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
======================
2+
Python Github Webhooks
3+
======================
4+
5+
Simple Python WSGI application to handle Github webhooks.
6+
7+
Dependencies
8+
============
9+
10+
::
11+
12+
sudo pip install Flask
13+
14+
Install
15+
=======
16+
17+
18+
Setup
19+
=====
20+
21+
This application will execute scripts in the hooks directory using the
22+
following order:
23+
24+
::
25+
26+
hooks/{event}-{name}-{branch}
27+
hooks/{event}-{name}
28+
hooks/{event}
29+
hooks/all
30+
31+
32+
License
33+
=======
34+
35+
::
36+
37+
Copyright (C) 2014 Carlos Jenkins <carlos@jenkins.co.cr>
38+
39+
Licensed under the Apache License, Version 2.0 (the "License");
40+
you may not use this file except in compliance with the License.
41+
You may obtain a copy of the License at
42+
43+
http://www.apache.org/licenses/LICENSE-2.0
44+
45+
Unless required by applicable law or agreed to in writing,
46+
software distributed under the License is distributed on an
47+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
48+
KIND, either express or implied. See the License for the
49+
specific language governing permissions and limitations
50+
under the License.
51+

config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"github_ips_only": false,
3+
"enforce_secret": "",
4+
}

hooks/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore

webhooks.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) 2014 Carlos Jenkins <carlos@jenkins.co.cr>
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
import hmac
19+
from hashlib import sha1
20+
from json import loads, dumps
21+
from ip_address import ip_address, ip_network
22+
from shlex import split as shsplit
23+
from os import access, X_OK
24+
from os.path import isfile, abspath, normpath, dirname, join
25+
26+
from flask import Flask, request, abort
27+
28+
29+
application = Flask(__name__)
30+
31+
32+
@app.route('/', methods=['GET', 'POST'])
33+
def index():
34+
"""
35+
"""
36+
hooks = join(normpath(abspath(dirname(__file__))), 'hooks')
37+
38+
whitelist = requests.get('https://api.github.com/meta').json()['hooks']
39+
40+
# Only POST is implemented
41+
if request.method != 'POST':
42+
abort(501)
43+
44+
# Load config
45+
with open('config.json', 'r') as cfg:
46+
config = loads(cfg.read())
47+
48+
# Allow Github IPs only
49+
if config.get('github_ips_only', True):
50+
src_ip = ip_address(request.remote_addr)
51+
for valid_ip in whitelist:
52+
if src_ip in ip_network(valid_ip):
53+
break
54+
else:
55+
abort(403)
56+
57+
# Enforce secret
58+
secret = config.get('enforce_secret', '')
59+
if secret:
60+
# Only SHA1 is supported
61+
sha_name, signature = request.headers.get('X-Hub-Signature').split('=')
62+
if sha_name != 'sha1':
63+
abort(501)
64+
65+
# HMAC requires its key to be bytes, but data is strings.
66+
mac = hmac.new(secret, msg=data, digestmod=sha1)
67+
if not hmac.compare_digest(mac.hexdigest(), signature):
68+
abort(403)
69+
70+
# Implement ping
71+
event = request.headers.get('X-GitHub-Event', None)
72+
if event == 'ping':
73+
return dumps({'msg': 'pong'})
74+
75+
# Gather data
76+
try:
77+
payload = loads(request.data)
78+
meta = {
79+
'name': payload['repository']['name'],
80+
'branch': payload['ref'].split('/')[2],
81+
'event': event
82+
}
83+
except:
84+
abort(400)
85+
86+
# Possible hooks
87+
scripts = [
88+
join(hooks, '{event}-{name}-{branch}'.format(**meta)),
89+
join(hooks, '{event}-{name}'.format(**meta)),
90+
join(hooks, '{event}'.format(**meta)),
91+
join(hooks, 'all')
92+
]
93+
94+
# Run scripts
95+
for s in scripts:
96+
if isfile(s) and access(s, X_OK):
97+
call(
98+
shsplit("{} '{}'".format(s, dumps(payload))),
99+
shell=True
100+
)

0 commit comments

Comments
 (0)