Relaying Github Webhooks

Programming is my primary interest but Linux server administation is another. I find webhooks a valuable way of keeping up to date and more recently a convenient way to receive updates from Github.

For this purpose I decided a centralized system would benefit me. First of all it means I can configure all webhooks from one location. As well as this, it allows me to parse and possibly modify a webhooks payload before passing it onto Discord.

To give an example of what I mean, there's an awesome backup utility for Linux called GoBackup. It includes the ability to notify, by webhook, completion status to various platforms including Discord. However, the default webhook message resembles:

systemd: [GoBackup] OK: Backup service has successfully

Backup of service completed successfully at 2023-10-23 09:17:47.443308376 +0100 BST

Which is fine, it gives plenty of detail and may be all one needs. However, I prefer to use notifications that take the form:

<hostname> :: <Service>: <status message>

With the origin of the webhook (server hostname), followed by the Linux service name and finally the status message. I already get an idea of the time from Discord and if I need further information I check logs.


Defining the route and handler

I chose Flask for this task. First step, define the endpoint:

@app.route("/github-payload", methods=["POST"])
def github_payload():
    ...

Then we'll need to parse the event type using the X-Github-Event header:

    payload = request.get_json()
    match event_type := request.headers.get("X-GitHub-Event"):
        case "issues":
            issues_handler(payload)
    return ("", 200, None)

And finally define a handler for this event type:

def issues_handler(payload):
    embed = {
        ...
    }
    data = {
        "username": "Github-OpenGist",
        "embeds": [embed],
    }
    send_discord_webhook(app.config["DISCORD_WEBHOOK"], data)

def send_discord_webhook(webhook_url, data):
    requests.post(webhook_url, json=data)

The end result is a webhook message that closely resembles a webhook message directly from Github.


Verifying the request

Since I intend to sit this Flask server behind a reverse proxy I took it one step further. By fowarding the IP of the request we can check the requests ip against Github's Hook servers with the endpoint https://api.github.com/meta:

def verify_src_ip(src_ip):
    allowed_ips = requests.get("https://api.github.com/meta").json()["hooks"]
    return any(src_ip in ip_network(valid_ip) for valid_ip in allowed_ips)

Github also allows us to associate a secret with the webhook which we can verify like so:

def verify_hmac_hash(data, signature):
    github_secret = bytes(app.config["GITHUB_SECRET"], "ascii")
    mac = hmac.new(github_secret, msg=data, digestmod=hashlib.sha1)
    return hmac.compare_digest("sha1=" + mac.hexdigest(), signature)

Conclusion

With everything up and running I can configure/modify and receive webhook notification through a proxy server.

I've posted a partial implementation of the code, only Github Issues are defined but it can be extended to handle other event types. In my case I've added more routes for linux services.

Notes about the gist: