Payload server in Python 3 for Github webhooks

July 9th, 2014 at 5:50 am

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.

Related posts:

  1. Twisted-based IRC server example
  2. Local execution of Python CGI scripts – with Python 3
  3. MySQL server installation woes
  4. Moving to Github – one year recap
  5. Switching my open-source projects from Bitbucket to Github

Leave a Reply

To post code with preserved formatting, enclose it in `backticks` (even multiple lines)