SSL/TLS-Netzwerkprogrammierung mit Boost.Asio, Teil 3: Client-Programmierung und Fehlerbehandlung

Boost.Asio bietet plattformübergreifende Ansätze zur Netzwerkprogrammierung in C++, inklusive der Implementierung von SSL/TLS. Die Portabilität hat jedoch einige Grenzen, wie das Beispiel eines Clients zeigt.

Know-how  –  0 Kommentare


Nach den Grundlagen im ersten und dem Server im zweiten Beitrag fehlt nun noch ein mit Boost.Asio implementierter Client. Neben dessen Programmierung geht der letzte Teil der Artikelserie auf die Grenzen der Portabilität und die Fehlerbehandlung in der SSL/TLS-Entwicklung mit Boost.Asio ein.

Der WOPR-Client befindet sich im gleichen Tarball wie der Server. Eine Anleitung zum Kompilieren unter Unix/POSIX-kompatiblen Systemen und mit Visual Studio unter Windows steht im ersten Teil der Artikelreihe.

SSL/TLS-Netzwerkprogrammierung mit Boost.Asio

Teil 1: Grundlagen
Teil 2: Server-Programmierung
Teil 3: Client-Programmierung und Fehlerbehandlung

Im Unterverzeichnis des entpackten Tarball liegt der WOPR-Client in zwei Ausführungen: eine POSIX-kompatible und eine portable Version. Erstere arbeitet auf Linux, Unix und POSIX-Subsystemen wie Cygwin. Die zweite Variante arbeitet portabel auf allen Plattformen, auf denen Boost.Asio verfügbar ist, darunter auch natives Windows ohne POSIX-Aufsatz.

Der POSIX-Client (terminal.cpp) ist in der Lage, Tastatureingaben ebenso asynchron zu verarbeiten wie Schreib-/Leseereignisse auf dem Socket. Die portable Variante (sterminal.cpp) liest hingegen direkt vom Standardeingabekanal (STDIN), also ohne hierfür Boost.Asio zu nutzen. Dieser Client läuft somit auch auf Plattformen, die keinen einfachen Weg für asynchrones Lesen von STDIN bieten. Hinsichtlich der Netzwerkfunktionen unterscheiden sich die beiden Clients nicht.

Zunächst steht die portable Implementierung aus sterminal.cpp im Fokus. Für den Client ist das Instanziieren und Nutzen eines io_service-Objekts ebenso obligatorisch wie für den Server. Auch beim Starten von run() verhalten die Beiden sich gleich. Interessant sind die Unterschiede: Bereits in der Hauptroutine main() erzeugt der Client einen Endpunkt und einen SSL/TLS-Kontext:

boost::asio::ip::tcp::resolver resolver(io_service);
boost::asio::ip::tcp::resolver::query query(host, port);
boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve(query);
boost::asio::ssl::context ctx(io_service, boost::asio::ssl::context::sslv23);
ctx.set_verify_mode(boost::asio::ssl::context::verify_peer);
ctx.load_verify_file(cert_file);

Das Erzeugen des Endpoint übernimmt ein TCP-Resolver der Klasse boost::asio::ip::tcp::resolver. Er dient schlicht zur Namensauflösung per DNS, hosts-Datei und/oder NIS; je nachdem was als Resolver auf dem System eingestellt ist. Nach dem Erzeugen einer Query mit Hostname und Port löst der Resolver sie als Iterator auf, der eine Liste von Endpoints für den Hostnamen zurückgibt. Das können durchaus mehrere sein, beispielsweise durch die Round-Robin-Einträge im DNS.

Sollte die Auflösung fehlschlagen, wirft die oben verwendete Form von resolve() eine Exception vom Typ boost::system::system_error. Das ist an dieser Stelle OK, da sich der Programmlauf noch nicht in io_service::run() befindet. Zusätzlich existiert eine Variante, die keine Exception wirft, sondern einen Fehlercode zurückliefert, und somit innerhalb der asynchronen Verarbeitung genutzt werden kann.

Nach dem Erzeugen des Iterators entsteht der SSL-Kontext. Dabei aktiviert der set_verify_mode() die Verifikation des Server-Zertifikats. Um sie zu ermöglichen, lädt der Kontext die Zertifikatskette aus der Datei cert_file. Anschließend erfolgt die Instanziierung der Klasse WoprPortableTerminal, die den Client implementiert. Ähnlich wie beim Server stößt der Konstruktor bereits die erste asynchrone Operation an. Nach dem anschließenden io_service.run() kann die asynchrone Verarbeitung starten.