The Github Webhooks API is powerful and flexible, making it simple to integrate services with your source repository. Lately I've been tinkering with it a bit, but all the examples Github has are in Ruby. So I put together a simple demo server in Python 3. Though simple (it's completely self contained and only needs Python 3 to run), it's complete, covering even webhook security by verifying the signature created with the API's secret token.

Here it is:

import argparse
import hashlib
import hmac
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import pprint
import os
import sys

# It's not recommended to store the key within the code. Following
# http://12factor.net/config, we'll store this in the environment.
# Note that the key must be a bytes object.
HOOK_SECRET_KEY = os.environb[b'HOOK_SECRET_KEY']


class GithubHookHandler(BaseHTTPRequestHandler):
    """Base class for webhook handlers.

    Subclass it and implement 'handle_payload'.
    """
    def _validate_signature(self, data):
        sha_name, signature = self.headers['X-Hub-Signature'].split('=')
        if sha_name != 'sha1':
            return False

        # HMAC requires its key to be bytes, but data is strings.
        mac = hmac.new(HOOK_SECRET_KEY, msg=data, digestmod=hashlib.sha1)
        return hmac.compare_digest(mac.hexdigest(), signature)

    def do_POST(self):
        data_length = int(self.headers['Content-Length'])
        post_data = self.rfile.read(data_length)

        if not self._validate_signature(post_data):
            self.send_response(401)
            return

        payload = json.loads(post_data.decode('utf-8'))
        self.handle_payload(payload)
        self.send_response(200)


class MyHandler(GithubHookHandler):
    def handle_payload(self, json_payload):
        """Simple handler that pretty-prints the payload."""
        print('JSON payload')
        pprint.pprint(json_payload)


if __name__ == '__main__':
    argparser = argparse.ArgumentParser(description='Github hook handler')
    argparser.add_argument('port', type=int, help='TCP port to listen on')
    args = argparser.parse_args()

    server = HTTPServer(('', args.port), MyHandler)
    server.serve_forever()

Just run it at some port on your server and point the webhook you create to it. Currently it just runs on the server's root path (e.g. http://myserver.com:1234), but should be trivial to modify to any path.

By the way, I found ngrok to be invaluable for testing this. It creates a tunnel from your localhost's port to a unique URL you can set as the webhook destination on Github. This makes it possible to quickly iterate and test the server on your local machine.


Comments

comments powered by Disqus