Let’s encrypt Suave: Der andere Weg

Beim letzten Mal haben wir aufgehört, als wir ein gültiges Zertifikat für unseren Server von Let’s encrypt ausgestellt bekommen haben. Momentan haben die ausgestellten Zertifikate nur einer Gültigkeitsdauer von drei Monaten. Daher ist das Ziel für den heutigen Artikel, den Prozess zu automatisieren.

Integration des Zertifikats

Suave benutzt das Zertifikat bei der Konfiguration der HTTPS Bindings. Meines Wissens ist keine Möglichkeit vorgesehen, das Zertifikat im laufenden Betrieb auszutauschen. Eine mögliche Richtung wäre, den DefaultTlsProvider zu wrappen und eine eigene Zuordnung von Binding zu Zertifikat zu verwenden.

Für mich ist das allerdings keine Notwendigkeit. Ich kann erst mal damit leben, dass ich den Server neu starten muss, wenn ich ein neues Zertifikat verwenden muss.

Daraus ergibt sich dann der erste Entwurf für den Typ unserer angestrebten Funktion.

Die Funktion nimmt eine Funktion entgegen und liefert unit zurück. Dass unit zurückgegeben wird lässt sich leicht erklären, da wir ja hier einen Seiteneffekt produzieren. Wir starten einen Webserver. Beim ersten Argument, der Funktion, habe ich zwei Parameter vorgesehen und einen asynchronen Workflow als Rückgabe. Der erste Parameter ist das Zertifikat, das wir von Let’s encrypt bekommen werden. Das CancellationToken ergibt auf den ersten Blick vielleicht keinen Sinn, aber das hängt damit zusammen, dass wir Suave wrappen. Damit wir, wenn es Zeit ist das Zertifikat zu aktualisieren, den Server beenden können, muss er auf das CancellationToken hören. Suave hört defaultmäßig auf das DefaultCancellationToken von F#, aber es ist generell schlecht, ein lokales Problem global zu lösen. Man weiß ja nicht, welche Async Workflows damit noch unabsichtlich abgebrochen werden.

Damit haben wir den wichtigsten Teil unserer API festgelegt. Als nächstes modellieren wir unseren Update-Prozess.

Typenbasiertes Design

Im Grunde können wir beim Design mit unseren möglichen Zuständen beginnen. Also sammeln wir erst mal, durch welche Zustände der automatische Zertifikatsprozess kommt.

StartzustandKonfiguration vorhanden, aber noch kein Account angelegt
RegistriertRegistration abgeschlossen, aber aktuell kein gültiges Zertifikat geholt
ZertifiziertGültiges Zertifikat geholt und installiert

Diese Zustände können wir wie folgt in F# Datentypen abbilden:

Configuration enthält alle notwendigen Daten, um die Registrierung und Authentifizierung bei Let’s encrypt durchzuführen. Ich habe das dieses Mal ausgelassen, da wir diesen Teil ja schon beim letzten Mal betrachtet hatten. Account ist momentan nur ein Container für den privaten Schlüssel mit dem wir unsere HTTP Anfragen an die Let’s encrypt API durchführen.

Certificate kommt im Registriert Zustand und Zertifiziert zu tragen, wenn wir entweder ein gültiges Zertifikat abgeholt haben, oder wenn wir momentan kein gültiges haben. Entweder weil wir es noch gar nicht gemacht haben, oder weil das letzte gültige Zertifikat abgelaufen ist.

RegisteredData und Registration bilden den Startzustand und Registriert ab. Wenn wir zum ersten Mal starten sind wir Unregistered. Sobald wir uns einmal mit den Daten der Configuration angemeldet haben sind, sind wir Registered.

Damit können wir unsere Zustände abbilden, aber ohne Übergänge zwischen den Zuständen wäre unser Programm sehr statisch. Daher kümmern wir uns als nächstes um die Funktionen für die Zustandsübergänge.

Zustandsübergänge und Async Workflows

Im Buch Real-World Functional Programming von Tomas Petricek und Jon Skeet habe ich vor einer längeren Weile mal ein Kapitel gelesen, wo die Autoren einen einfachen Zustandsautomaten mit Hilfe von rekursiven asynchronen Workflows umgesetzt hatten (Kapitel 16 Developing functional reactive programs).

Das Konzept ist mir in Erinnerung geblieben, aber ich hatte bisher keine Gelegenheit das Konzept selbst anzuwenden. Das ändert sich nun.

Hier eine kurze Zusammenfassung, damit der nächste Codeblock hoffentlich mehr Sinn macht.

Beispiel für Zustandsautomaten mit Async

Angenommen wir haben zwei Zustände: State A und State B. Und wir wechseln von A nach B, wenn 10 Sekunden vergangen sind und von B nach A zurück, nach 5 Sekunden. Anstelle des Wartens könnten hier natürlich beliebig komplexe Bedingungen stehen, aber um das Beispiel kurz zu halten, belassen wir es mal beim Warten.

Ich hoffe mit diesem kleinen Beispiel vorab, wird der folgende Code dann jetzt nachvollziehbarer.

Unser Zustandsautomat

Ich werde den gesamten Block in separate Schnipsel zerlegen, damit wir gleich einfacher drüber gehen können. In der Quellcodedatei sind diese Schnipsel zusammenhängend.

Hier haben wir den ersten Teil. Im ersten Fall benutzen wir die Funktion vom letzten Mal um ein Zertifikat anzufordern und wechseln dann in den Zustand Registered, in dem wir uns rekursiv selbst aufrufen. Im zweiten Fall haben wir ein gültiges Zertifikat und können Suave starten. Die callback Funktion sollte bekannt sein aus Code Listing 1. Der Aufruf entspricht der dort definierten Signatur, bis auf den Rückgabetyp, dazu komme ich später nochmal. Aber damit wir später unterscheiden können, ob der Server beendet wurde oder das Zertifikat abgelaufen ist, habe ich noch einen zusätzlichen Typen definiert.

Anschließend warten wir darauf, dass der Server gestoppt wird, oder das Zertifikat ablaufen wird. Wenn der Server gestoppt werden soll, geben wir das erst mal nur weiter. Im anderen Fall wechseln und stoppen wir den Server indem wir unsere CancellationToken aktivieren. Dann warten wir, bis der Server fertig runtergefahren wurde und wechseln dann in den Zustand NotRequested.

Die Funktion waitCertExpiration berechnet die Dauer, bis das Zertifikat abgelaufen sein wird und legt sich so lange schlafen.

Der Zustand Unregistered ist recht einfach. Wir registrieren uns und wechseln in den Zustand Registered.

 

Als finales Stück habe ich noch eine Einstiegsfunktion, die es ermöglicht auch gleich im Zustand Registered zu starten.

Fazit

Neben den hier gezeigten, aus meiner Sicht relevanten Stellen gibt es natürlich noch einiges an zusätzlichem Code, wie die Persistenz für den Account und das Zertifikat. Nichtsdestotrotz besteht die gesamte Lösung aus nur knapp 500 Zeilen Code. Wer den Rest noch sehen möchte, findet ihn hier.

Neben meinen Missverständnissen die mit Async hatte, war die Implementierung eigentlich auch recht gradlinig. Die Möglichkeit den Zustandsautomaten über Async Workflows umzusetzen hat mir sehr gefallen. Die gleiche Möglichkeit besteht im Übrigen auch für die C# Entwickler unter euch mit async/await.

Vielen Dank für’s Lesen

P.S. Noch zu dem abweichenden Rückgabetyp: Als ich Async.Choice verwenden wollte, habe ich gemerkt, dass unit als Rückgabe zwar funktioniert, aber anders als bei Task.WhenAny aus der TPL, wo man den Task bekommt der fertig wurde, liefert Choice nicht den fertigen Async Workflow selbst zurück, sondern das Ergebnis des Workflows.

Da die Ergebnistypen gleich sein müssen, hätte ich nur zweimal () (den singulären Wert von unit) bekommen und hätte nicht entscheiden können, welcher Fall eingetreten ist. Daher habe ich einen neuen Typen eingeführt und die API dahingehend abgeändert, dass der Rückgabetyp von diesem neuen Typen ist.