Skip to content
This repository was archived by the owner on Oct 2, 2020. It is now read-only.

Commit 7a58add

Browse files
committed
Initial commit
0 parents  commit 7a58add

File tree

8 files changed

+204
-0
lines changed

8 files changed

+204
-0
lines changed

.gitignore

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
*.pyc
2+
bin/
3+
config.ini
4+
include/
5+
local/
6+
lib/
7+
*.swp
8+
*.rdb
9+
.idea/
10+
nbproject/
11+
MANIFEST
12+
dist/
13+
build/

LICENSE

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2015 Drew DeVault
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of
4+
this software and associated documentation files (the "Software"), to deal in
5+
the Software without restriction, including without limitation the rights to
6+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7+
of the Software, and to permit persons to whom the Software is furnished to do
8+
so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.

README.md

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Hooks
2+
3+
This is a simple web server that enables you to execute commands when you
4+
receive web hooks from Github. This lets you, for example, redeploy things when
5+
you push a commit.
6+
7+
## Installation
8+
9+
`pip install ghhooks` will do the trick, or just `sudo python ./setup.py
10+
install`. Hooks depends on Flask.
11+
12+
## Deployment
13+
14+
### Stupid way
15+
16+
Just run `ghhooks` and it'll work well enough.
17+
18+
### Smart way
19+
20+
Install gunicorn and run `gunicorn hooks:app`, then set up a web server like
21+
nginx to proxy to it (see [nginx](#nginx)). Use an init script from the init
22+
directory, or write your own and send a pull request adding it.
23+
24+
## Configuration
25+
26+
It looks for `config.ini` in the CWD or uses `/etc/hooks.conf` if that doesn't
27+
exist. An example is given in this repository as "config.ini":
28+
29+
[example-hook]
30+
repository=SirCmpwn/hook
31+
branch=master
32+
command=echo Hello world!
33+
valid_ips=204.232.175.64/27,192.30.252.0/22,127.0.0.1
34+
# Note: these IP blocks are Github's hook servers, plus localhost
35+
36+
You may add as many hooks as you like, named by the `[example-hook]` line.
37+
38+
Hooks will pass Github's JSON payload into your hook command.
39+
40+
### Github configuration
41+
42+
1. Go to https://github.com/OWNER/REPOSITORY/settings/hooks/new (modify this
43+
URL as appropriate)
44+
2. Set your payload URL to "http://your-server/hook"
45+
3. Set the content type to `application/json`
46+
4. Do not include a secret
47+
5. "Just the `push` event"
48+
6. Click "Add webhook" and you're good to go!
49+
50+
## Nginx
51+
52+
I suggest you run this behind nginx. The location configuration might look
53+
something like this:
54+
55+
location /hook {
56+
proxy_pass http://localhost:8000;
57+
}

ghhooks

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env python3
2+
from hooks import app
3+
4+
if __name__ == '__main__':
5+
app.run(host='0.0.0.0', port=8000, debug=True)

hooks.py

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from flask import Flask, request, abort
2+
from configparser import ConfigParser
3+
4+
import sys
5+
import os
6+
from subprocess import Popen, PIPE, STDOUT
7+
import urllib
8+
import json
9+
import logging
10+
11+
config_paths = ["./config.ini", "/etc/hooks.conf"]
12+
config = ConfigParser()
13+
for p in config_paths:
14+
try:
15+
config.readfp(open(p))
16+
break
17+
except:
18+
pass
19+
20+
app = Flask(__name__)
21+
22+
class Hook():
23+
def __init__(self, name, config):
24+
self.name = name
25+
self.repository = config.get(name, "repository")
26+
self.branch = config.get(name, "branch")
27+
self.command = config.get(name, "command")
28+
self.valid_ips = config.get(name, "valid_ips")
29+
30+
hooks = list()
31+
32+
for key in config:
33+
if key == 'DEFAULT':
34+
continue
35+
hooks.append(Hook(key, config))
36+
37+
print("Loaded {} hooks".format(len(hooks)))
38+
39+
def makeMask(n):
40+
return (2 << n - 1) - 1
41+
42+
def dottedQuadToNum(ip):
43+
parts = ip.split(".")
44+
return int(parts[0]) | (int(parts[1]) << 8) | (int(parts[2]) << 16) | (int(parts[3]) << 24)
45+
46+
def networkMask(ip, bits):
47+
return dottedQuadToNum(ip) & makeMask(bits)
48+
49+
def addressInNetwork(ip, net):
50+
return ip & net == net
51+
52+
@app.route('/hook', methods=['POST'])
53+
def hook_publish():
54+
raw = request.data.decode("utf-8")
55+
try:
56+
event = json.loads(raw)
57+
except:
58+
return "Hook rejected: invalid JSON", 400
59+
repository = "{}/{}".format(event["repository"]["owner"]["name"], event["repository"]["name"])
60+
matches = [h for h in hooks if h.repository == repository]
61+
if len(matches) == 0:
62+
return "Hook rejected: unknown repository {}".format(repository)
63+
hook = matches[0]
64+
65+
allow = False
66+
for ip in hook.valid_ips.split(","):
67+
parts = ip.split("/")
68+
range = 32
69+
if len(parts) != 1:
70+
range = int(parts[1])
71+
addr = networkMask(parts[0], range)
72+
if addressInNetwork(dottedQuadToNum(request.remote_addr), addr):
73+
allow = True
74+
if not allow:
75+
return "Hook rejected: unauthorized IP", 403
76+
77+
if any("[noupdate]" in c["message"] for c in event["commits"]):
78+
return "Hook ignored: commit specifies [noupdate]"
79+
80+
if "refs/heads/" + hook.branch == event["ref"]:
81+
print("Executing hook for " + hook.name)
82+
p=Popen(hook.command.split(), stdin=PIPE)
83+
p.communicate(input=raw.encode())
84+
return "Hook accepted"
85+
86+
return "Hook ignored: wrong branch"

init/hooks.service

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[Unit]
2+
Description=Github hook service
3+
Wants=network.target
4+
Before=network.target
5+
6+
[Service]
7+
Type=simple
8+
ExecStart=/usr/bin/gunicorn hooks:app
9+
ExecStop=/usr/bin/pkill gunicorn
10+
11+
[Install]
12+
WantedBy=multi-user.target

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Flask==0.10.1

setup.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from distutils.core import setup
2+
setup(name="ghhooks",
3+
author="Drew DeVault",
4+
author_email="[email protected]",
5+
url="https://github.com/SirCmpwn/hooks",
6+
description="Executes commands based on Github hooks",
7+
license="MIT",
8+
version="1.0",
9+
scripts=["ghhooks"],
10+
py_modules=["hooks"],
11+
install_requires=["Flask"])

0 commit comments

Comments
 (0)