Simple Flask Webhook

Introduction

Since this is my first technical blog post, I though I'd start with something relatively simple. Also, please bare with me as I get to grips with the formatting of this confounded contraption!
If you have any feedback, then please feel free to send me a mail at james.innes@ogma-dev.com.

In this post, I'm going to give a quick guide on how to get a fairly simple, yet incredibly useful, webhook setup with Flask.

Source Code

You can find the complete source code here: https://github.com/Ogma-Dev/Simple-Flask-Webhook

What is a Webhook?

Very basically; a webhook is a HTTP web service that listens on a server and will respond and/or perform a task when they are called by some event. Read more.

What is Flask?

Flask is a micro web framework written in Python. Read more.

Prerequisites

Since you're here, I'm going to assume you have Python installed. Python 2 is soon(ish...) to be depreciated, so I'm going to be using Python 3 here (and in the future).

Virtual Environments

In my opinion it's always good practice to use virtual environments. I currently use Ubuntu, so the following examples to setup will be with aptitude, but I'm sure it's mostly translatable to whichever package manager your linux distribution uses.

If you don't have virtual environments setup on your system, you should install the virtualenv package with:

$ sudo apt-get install virtualenv

We'll get into making the virtual environment in a little bit... For now, you just need it installed.

Let's Get Started

Ok, with the introduction out of the way, let's get started...

Setup

I like to start all new applications in a clean folder of their own... So, that's where we'll start. Make a new directory and let's call is simple_flask_webhook, then change directory into that folder;

$ mkdir simple_flask_webhook
$ cd simple_flask_webhook

Once in here, we will setup our virtual environment for our webhook;

$ virtualenv --python=python3 venv

Activate the virtual environment (i.e. set it as your working installation of Python);

$ source venv/bin/activate

You should now have a (venv) infront of your bash prompt (if you don't have any shell fanciness!).

Python Packages

With the virtual environment created and activated we need to install Flask;

(venv)$ pip install flask

If all went well and flask installed, your output of pip freeze should look something like this:

(venv)$ pip freeze
Flask==0.10.1
itsdangerous==0.24
Jinja2==2.8
MarkupSafe==0.23
pkg-resources==0.0.0
Werkzeug==0.11.9

Note: Don't worry about the version numbers, by the time you read this it might be a bit old!

The Webhook

Right, now let's get to the good stuff!

Create a new file, webhook.py;

(venv)$ touch webhook.py

Open it up with your favourite editor, and we'll start writing our app...

from flask import Flask, request, abort

app = Flask(__name__)


@app.route('/webhook', methods=['POST'])
def webhook():
    if request.method == 'POST':
        print(request.json)
        return '', 200
    else:
        abort(400)


if __name__ == '__main__':
    app.run()

Hah! I told you it was simple! The webhook will print out any json you send to it. You can test it out with curl.
i.e. curl -H "Content-Type: application/json" -X POST -d '{"data": "This is some test data"}' 127.0.0.1:5000/webhook

Adding a Little Validation

Well, that's ok; but perhaps a little more security wouldn't go a miss... Let's implement a simple token validation;

import os
from datetime import datetime, timedelta
from flask import Flask, request, abort, jsonify

def temp_token():
    import binascii
    temp_token = binascii.hexlify(os.urandom(24))
    return temp_token.decode('utf-8')

WEBHOOK_VERIFY_TOKEN = os.getenv('WEBHOOK_VERIFY_TOKEN')
CLIENT_AUTH_TIMEOUT = 24 # in Hours

app = Flask(__name__)

authorised_clients = {}


@app.route('/webhook', methods=['GET', 'POST'])
def webhook():
    if request.method == 'GET':
        verify_token = request.args.get('verify_token')
        if verify_token == WEBHOOK_VERIFY_TOKEN:
            authorised_clients[request.remote_addr] = datetime.now()
            return jsonify({'status':'success'}), 200
        else:
            return jsonify({'status':'bad token'}), 401

    elif request.method == 'POST':
        client = request.remote_addr
        if client in authorised_clients:
            if datetime.now() - authorised_clients.get(client) > timedelta(hours=CLIENT_AUTH_TIMEOUT):
                authorised_clients.pop(client)
                return jsonify({'status':'authorisation timeout'}), 401
            else:
                print(request.json)
                return jsonify({'status':'success'}), 200
        else:
            return jsonify({'status':'not authorised'}), 401

    else:
        abort(400)

if __name__ == '__main__':
    if WEBHOOK_VERIFY_TOKEN is None:
        print('WEBHOOK_VERIFY_TOKEN has not been set in the environment.\nGenerating random token...')
        token = temp_token()
        print('Token: %s' % token)
        WEBHOOK_VERIFY_TOKEN = token
    app.run()

Wooft, well; that's a bit meatier. So, what's going on here...
When you start the app, it will check for an Environment Variable, called WEBHOOK_VERIFY_TOKEN, if there isn't one set it will automatically generate a token for you.
We initialise a dictionary of authorised_clients, which stores IP addresses (key) and the time they validated (value).
In order for an IP to become validated, they need to sent a GET request with a parameter verify_token equal to our webhooks token.
i.e. curl 127.0.0.1:5000/webhook?verify_token=YOUR_TOKEN_HERE
Once an IP is registered the webhook will accept POSTs from it, the same as previously. It's not exactly Fort Knox, but it'll suffice for now.

Further Improvements

This post is only supposed to give a brief introduction to writing webhooks and there are many improvements you could make which are probably outwith the scope of this post. However, here are a few suggestions to get you started!

Persistent Authorised Clients

Since we only store the autorised IP addresses in a dictionary, they're lost whenever the application closes. You could store these into a file format, or even a database. Flask-SQLAlchemy makes simplifies using databases with Flask!

Added Authentication

If you want to add further authentication, I'd recommend checking out Flask-HTTPAuth.

HTTPS

If you want to enable HTTPS (without using a full blown http service like nginx), pass your SSL certificates to Flask by modifying the run line to; app.run(ssl_context=(ssl_cert_file, ssl_key_file)) where the variables are paths to your certificates.
LetsEncrypt is a really quick and easy way to get setup with some certs!

End Notes

I hope you find this useful, and if you have any comments or have built on the examples and would like to share please feel free to drop me a line.