Suchen

Einfache Server-zu-Server-Kommunikation, Teil 2 Webhooks für das GitHub-Repository

Autor / Redakteur: Mirco Lang / Stephan Augsten

GitHub ist für nicht wenige Entwickler das Zentrum ihrer Arbeit – und wenn dort im Repo etwas passiert, möchte man auch darüber informiert werden. Mit Webhooks und einem Sinatra-Server lässt sich jede Repo-Aktivität in Echtzeit auswerten.

Firma zum Thema

Ein GitHub-Webhook lsst sich einfach mit ngrok und Sinatra umsetzen.
Ein GitHub-Webhook lsst sich einfach mit ngrok und Sinatra umsetzen.
(Bild: Lang)

Es gibt heute massenhaft Möglichkeiten, Tools miteinander zu verbinden oder über Ereignisse auf welcher Plattform auch immer in nahezu beliebiger Manier benachrichtigt zu werden – alle mir Vor- und Nachteilen. Wenn Sie mit GitHub arbeiten und sich bei der lokalen Entwicklung komplett auf git und Texteditoren beschränken, bieten sich Webhooks als eine Variante an.

Das Schöne daran: Sie können es auch wunderbar einfach daheim aufsetzen oder einfach ad-hoc, wenn Sie mal kurzfristig mit Webhooks und automatischen Benachrichtigungen spielen wollen. Ein neuer Pull Request im Repo? Wenn Sie dazu einfach nur eine Mail oder eine Telegram-Nachricht bekommen wollen, könnten Sie – wie im ersten Teil zu den Webhooks gezeigt – auch schlicht auf den Dienst If-This-Then-That setzen: WENN ein Pull Request im Repo auftauch, DANN schicke eine Nachricht an ABC.

Aber vielleicht möchten Sie ja nur lokal im Terminal benachrichtigt werden. Oder die anfallenden Daten anderweitig verarbeiten. Dann müssen Sie selbst einen Webhook anbieten, über den GitHub neue Ereignisse melden kann – was die Plattform erfreulicherweise beherrscht. Im Folgenden zeigen wir also, wie Sie Webhooks auf GitHub konfigurieren und Anfragen darüber lokal empfangen und auswerten.

Sofern Sie einen GitHub-Account haben, können Sie das Ganze direkt nachspielen, denn lokal benötigen Sie lediglich ein Linux und die beiden Tools ngrok und Sinatra, die Sie beide problemlos per Paketmanager installieren können. Auch Portweiterleitungen oder Firewall-Regeln müssen Sie nicht separat anpassen, für die Verbindung zur Außenwelt sorgt ngrok.

GitHub-Benachrichtigungen aufsetzen

Der Work-, oder besser gesagt Dataflow, läuft später wie folgt: In Ihrem GitHub-Repository geschieht irgendetwas – beispielsweise ein simpler Commit. Daraufhin wirft GitHub die hinterlegte Webhook-Endpunkt-URL an und schickt die Repo-Daten mit. Die Webhook-Endpunkt-URL stellt der Tunneling-Webservice ngrok zur Verfügung, der für die Exponierung des Servers verantwortlich ist.

Hinter dem exponierten Port wartet dann Sinatra auf HTTP-POST-Abfragen. Die Sinatra-App kann im einfachsten Fall aus gerade mal sechs Zeilen Ruby bestehen, um eine Nachricht und die ankommenden Daten auszugeben. Auch wenn GitHub es selbst gerne anders herum erläutert, beginnen Sie am besten mit Sinatra und ngrok.

Der Grund dafür besteht darin, dass man in der GitHub-Webhook-Konfiguration eine gültige Endpoint-URL angeben muss, die eben erst von ngrok bereitgestellt wird. Installieren Sie zunächst Sinatra [http://sinatrarb.com/], unter Ubuntu funktioniert das ohne weitere Vorarbeit über den Ruby-Paketmanager, dafür genügt ein simples „gem install sinatra“.

Ngrok können Sie von der Website herunterladen und dann schlicht die enthaltene Binary „ngrok“ entpacken. Sie können ngrok auch direkt unter Angabe eines Protokolls und Ports starten: „ngrok http 4567“. Anschließend sehen Sie schon, wie ngrok funktioniert – anhand der Meldung:

Forwarding         http://141e2e1c7849.ngrok.io -> http://localhost:4567

Die Webhook-Konfiguration in GitHub samt ngrok-URL.
Die Webhook-Konfiguration in GitHub samt ngrok-URL.
(Bild: Lang / GitHub)

ngrok stellt also schlicht eine persönliche ngrok.io-Adresse zur Verfügung, die auf Ihren lokalen Server und den angegebenen Port weiterleitet, ein Tunnel. Der kostenlose ngrok-Service ist für Tests und ad-hoc-Installationen ausreichend, für die dauerhafte Nutzung gibt es kostenpflichtige Pläne. Vorsicht: Wenn Sie ngrok neu starten, wird eine neue URL generiert – und da diese als Webhook-Endpoint dient, würde der konfigurierte GitHub-Webhook ins Leere laufen.

Jetzt, da Sie die Endpoint-URL haben, können Sie sich in Ihrem GitHub-Repo einloggen und dort dessen Einstellungen aufrufen. Sie finden hier in der Navigation den Punkt Webhooks. Geben Sie hier folgende Daten an:

  • Payload URL: die ngrok-URL plus /payload
  • Content type: Application/json
  • Trigger: Send me everything
  • Active: Ja

In dieser Konfiguration wird GitHub den Webhook bei jeder Aktivität im Repo triggern. Alternativ könnten Sie freilich auch nur bestimmte Ereignisse melden lassen. Als Trigger stehen zum Beispiel auch Änderungen im Wiki, neue Forks, neue Deploy-Schlüssel oder natürlich auch neue Pull Requests zur Verfügung.

All diese Webhook-Events werden unterschiedlich angesprochen und liefern ebenso unterschiedliche Payloads – eine Übersicht findet sich auch bei GitHub. Nun ist der Server ist also dank ngrok erreichbar und GitHub feuert Webhooks ab – fehlt noch der Interpret, also eine Sinatra-App.

Wie simpel sich Webanwendungen mit Sinatra erstellen lassen, zeigt vermutlich am besten ein Beispiel. Legen Sie ein Skript an, etwa „webhooktest.rb“, und fügen Sie folgenden Inhalt ein:

require 'sinatra'
require 'json'
post '/payload' do
   push = JSON.parse(request.body.read)
   puts "Hier kommen die Daten: #{push.inspect}"
end

ngrok, der Sinatra-Code und der laufende Sinatra-Server – eigentlich simpel.
ngrok, der Sinatra-Code und der laufende Sinatra-Server – eigentlich simpel.
(Bild: Lang)

Der Aufbau des Ruby-Skripts ist denkbar simpel: Unter „post“ wird die lokale Webhook-Endpoint-URL spezifiziert – wenn also etwas über „http://141e2e1c7849.ngrok.io/payload“ eingeht, wird die folgende „do“-Anweisung ausgeführt. In diesem Fall heißt das, dass nach „Hier kommen die Daten:“ die Repo-Daten folgen, also beispielsweise Namen und Adresse eines Committers. Die „push“-Funktion speichert die JSON-Daten lediglich in einem Array.

GitHub führt Logs aller Webhook-Aktionen.
GitHub führt Logs aller Webhook-Aktionen.
(Bild: Lang / GitHub)

Zum Testen können Sie nun schlicht eine Readme-Datei im Repo verändern, eine Datei hinzufügen oder sonst eine Aktion durchführen – Sie sollten auf jeden Fall eine entsprechende Meldung im Terminal des laufenden Sinatra-Servers „webhooktest.rb“ sehen. Auch in GitHub selbst können Sie die Webhook-Aktionen nachvollziehen: Unter „Einstellungen/Webhooks“ erscheinen alle Log-Einträge am Ende der Seite, samt Status und Payload.

Sicherheit

In der Praxis sollten Sie nun noch eine Authentifizierung nutzen – theoretisch ganz simpel, praktisch wird es auf ein wenig Entwicklung mit Ruby, den JSON-Daten und Tools wie Base64 und OpenSSL hinauslaufen. GitHub selbst liefert eine simple Variante in der offiziellen Dokumentation, die allerdings genauso wenig funktioniert, wie auch andere Abschnitte darin. Selbst diverse Sinatra-Apps haben das Probleme beim Test nicht lösen können.

Stand jetzt funktioniert der Vergleich des übergebenen Schlüssels auf unserem Testsystem nur teilweise – irgendwo gibt es Probleme beim Auslesen und Verarbeiten des Schlüssels. Da die Verifikation aber von Ihrem eingesetzten System abhängig ist, ist das nicht weiter wild – um eigene Anpassungen kommen Sie sowieso nicht herum.

Schritt 1 für die Absicherung ist simpel: In der Webhook-Konfiguration auf GitHub legen Sie einfach im Feld „Secret“ einen beliebigen Schlüssel ab.

Schritt 2 findet dann in der Sinatra-App statt. GitHub verwendet den hinterlegten Schlüssel, um eine Signatur zu erzeugen und übergibt diese samt Payload über die Webhook-Endpoint-URL. Ihr Sinatra-App muss dann lediglich ebenfalls den Schlüssel bekommen, auf dieselbe Art wie GitHub verarbeiten und das eigene mit dem per Webhook bekommenen Ergebnis vergleichen. Der (minimal angepasste) Original-GitHub-Code sieht das folgendermaßen vor:

post '/payload' do
   request.body.rewind
   payload_body = request.body.read
   verify_signature(payload_body)
   push = JSON.parse(params[:payload])
   "Schlüssel akzeptiert, es kommt was an!"
end
def verify_signature(payload_body)
   signature = 'sha1=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), ENV['SECRET_TOKEN'], payload_body)
   return halt 500, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature, request.env['HTTP_X_HUB_SIGNATURE'])
end

Voraussetzung: Sie haben den Schlüssel zuvor in einer Umgebungsvariablen gespeichert: „export SECRET_TOKEN=mykey“ genügt dafür. Zum Testen könnten Sie den Schlüssel auch in das Ruby-Skript hart kodieren.

Die eigentliche Magie steckt in der Funktion „verify_signatures“, die hier die Signatur nachbaut (→ signature) und mit der Methode "Rack::Utils.secure_compare“ mit dem von GitHub gelieferten Wert (→ request.env['HTTP_X_HUB_SIGNATURE']) vergleicht.

Die Fehlermeldung aus dem Beispielcode bleibt im hiesigen Testszenario zwar auf der Strecke, aber immerhin: Stimmt der Schlüssel überein, gibt das Skript „Schlüssel akzeptiert, es kommt was an!“ aus – andernfalls bleibt es einfach stumm.

Letztlich ist aber Sinatra mit seinen Mini-Servern/Apps das eigentliche Highlight: Sie können problemlos Webhooks empfangen, sei es nun von GitHub oder sonst einer Quelle, und die Ergebnisse völlig frei via Ruby verarbeiten. Und da die Authentifizierung letztlich nur aus einem simplen Vergleich zweier kryptografisch erstellter Strings besteht, ist auch das keine Hürde.

Am Ende des Tages sollten vor allem eines hängen bleiben: Webhooks sind extrem flexibel, ziemlich simpel und sie ermöglichen ganze Ketten von Aktionen – schließlich können Webhooks auch Webhooks auslösen, die Webhooks auslösen, die ….

(ID:46899849)

Über den Autor

 Mirco Lang

Mirco Lang

Freier Journalist & BSIler