Neon ist ein ultrakompakter, modular aufgebauter HTTP-Server auf der Grundlage des Java-Moduls jdk.httpserver zum Einbetten in Apps und Microservices. In diesem Dokument sind Module zur Erweiterung von Neon beschrieben.

Module

Die folgenden Module sind in diesem Dokument beschrieben.

Für jedes der obigen Module bildet ein Java-Archiv eine Klassenbibliothek (.jar-Datei), die zur Nutzung einfach dem Classpath einer App oder eines Microservice hinzugefügt wird. Nachfolgend sind die Funktionen der Module ausführlicher beschrieben.

http-base

http-base stellt die zentrale Funktion bereit, die auf der Grundlage des Java-Modula jdk.httpserver einen Server für statische Dateien bildet. Das folgende Beispiel zeigt, wie das Modul eingebunden wird.

statische Dateien ausliefern mit Hilfe der Klasse FileHandler aus http-base
// einen Server auf Port 9191 erzeugen
HttpServer server = HttpServer.create(new InetSocketAddress(9191, 0);

// einen neuen HttpContext auf dem Endpunkt /meine-app erstellen
// und einen FileHandler daran binden
HttpContext context = server.createContext("/meine-app", new FileHandler());

// den Pfad zu den Daten setzen
context.getAttributes.put(FileHandler.ATTR_FILE_BASE, "/home/fred/www");

// den Server starten
server.setExecutor(Executors.newCachedThreadPool());
server.start();

Mit dem obigen Code wird die Klasse FileHandler des Moduls http-base für den Kontext /meine-app aktiviert. Die App liefert damit Dateien im Ordner /home/fred/www über den Port 9191 aus.

Wird der obige Code als Teil einer App oder eines Microservice ausgeführt, ist der Inhalt von /home/fred/www der betreffenden Maschine anschließend über den Uniform Resource Locator (URL) http://localhost:9191/meine-app/ via HTTP zugänglich.

Die Klasse FileHandler liefert abhängig von den Angaben im HTTP-Header ganze Dateien oder umfangreiche Inhalte als Streams aus.

Helfer

Neben der Hauptfunktion zum Ausliefern statischer Inhalte enthält das Modul http-base noch Helfer zur Unterstützung häufiger Aufgaben.

PatternDelegator

Mit der Klasse PatternDelegator können HttpHandler an Muster geknüpft werden, die aus regulären Ausdrücken gebildet werden. So lassen sich beispielsweise alle Ressourcen mit einer bestimmten Dateiendung oder gewissen Namensbestandteilen an einen Handler binden. Ist ein Handler zusätzlich von NeonHandler abgeleitet, können dem Handler eigene Attribute mitgegeben werden. Auf diese Weise können verschiedene Instanzen derselben Handler-Klasse mit unterschiedlichen Parametern genutzt werden, ohne, dass die Attribute sich mit denen eines HttpContext überschneiden.

HandlerDescriptor

Die Klasse HandlerDescriptor erlaubt im Zusammenspiel mit einem PatternDelegator die Deklaration einer Handler-Klasse und ihrer Attribute.

Mit dem Gespann aus HandlerDescriptor und PatternDelegator lassen sich leichtgewichtigere Server implementieren, da die Klasse PatternDescriptor einen Handler jeweils erst beim HTTP-Aufruf instanziiert. Zudem können zur Laufzeit dynamisch unterschiedliche Klassen eingesetzt werden ohne den Code neu kompilieren zu müssen. Handler-Klassen müssen zu diesem Zweck einen Konstruktor ohne Parameter besitzen.

http-realm

Sollen Inhalte nur bestimmten Nutzern zugänglich sein, muss eine Authentifizierung feststellen, welcher Nutzer eine Anwendung bedient. Die Angaben des Nutzers für die dafür notwendige Authentisierung prüft die Anwendung gegen ein Nutzerverzeichnis und eine einfache Variante ist im Modul http-realm implementiert.

Die Schnittstelle Realm muss von Anwendungen implementiert werden, wenn eine Authentifizierung gegen ein Nutzerverzeichnis im Sinne des Moduls http-realm verwendet werden soll. Die Klasse SimpleRealm implementiert beispielhaft einen Realm auf der Grundlage einer Datei.

Beispiel für die Nutzung einer Datei mit Nutzerdaten über die Klasse SimpleRealm
SimpleRealm realm = new SimpleRealm();
realm.readFromFile(new File("conf", my.realm));
realm.setName("/meine-app");

Für die Datei my.realm erwartet die Klasse SimpleRealm Einträge in folgender Struktur.

Struktur der Einträge im Benutzerverzeichnis für die Klasse SimpleRealm
# Benutzer meiner App
# [userId]=[password],[roleId],[roleId],etc.
test=test,testRolle
ulrich=geheim,testRolle,andereRolle
fred=undisclosed,nutzerRolle,adminRolle

Der Ausdruck vor dem Gleichheitszeichen ist die Nutzer-Kennung, die zur Anmeldung dient. Der erste Eintrag nach dem Gleichheitszeichen ist das Kennwort, gefolgt von einer Liste mit Komma getrennter Rollen-IDs. http-realm liefert damit zugleich die Ausgangsbasis für andere Implementierungen wie beispielsweise gegen eine Datenbank oder einen Web-Service.

Erweiterungen oder Abwandlungen der Klasse SimpleRealm sollten auch eine Form der Verschlüsselung für das Kennwort einbauen, damit es nicht im Klartext für andere lesbar in der Datei vorliegt.

http-oauth

Mit http-oauth wird Neon um eine Möglichkeit der Authentifizierung nach RFC 6750 Bearer Token Authentication erweitert. Dieses Modul basiert auf JJWT sowie Gson und erfordert neben der Datei http-oauth.jar die Dateien jjwt-api.jar, jjwt-impl.jar, jjwt-gson.jar sowie gson.jar und ferner die Module http-base und http-realm.

BearerAuthenticator

Zum Einschalten der Authentifizierung für einen bestimmten HTTP-Service-Endpunkt kann die Klasse BearerAuthenticator mit folgender Vorgehensweise dienen.

Beispiel zum Einschalten der Bearer Token Authentifizierung
// einen Authenticator gegen unser Nutzerverzeichnis erstellen
BearerAuthenticator auth = new BearerAuthenticator();
auth.setRealm(realm);
auth.setExpireSeconds(7200); // 2 Stunden
auth.setPrincipalAuthRealm("/meine-app");
auth.setWWWAuthRealm("/meine-app");
auth.setRefreshExpireSeconds(86400); // 1 Tag
auth.setRefreshSeconds(3600); // 1 Stunde bis nach dem Refresh gefragt wird

context = server.createContext("/meine-app/admin", new Dateiablage("/home/fred/daten", "adminRolle"));
// der Authenticator stellt sicher, dass Nutzer sich authentifizieren
context.setAuthenticator(auth);

Im obigen Code ist die Klasse Dateiablage ein Beispiel, das symbolisiert, dass hier eine Funktion verwendet wird, die an eine Rolle geknüpft ist. Diese Rolle muss ein Benutzer haben, wenn die Funktion genutzt werden soll. Die Überprüfung der Rolle ist nicht Teil des Beispiels, erfordert aber, dass Nutzer sich authentifizieren.

BearerLoginHandler

Ist eine Ressource mit der Klasse BearerAuthenticator verbunden, wird gemäß Spezifikation eine Antwort mit Status 401 Unauthorized gesendet, wenn eine HTTP-Anfrage ohne Authentifizerung für diese Ressource eingeht.

Antwort auf eine nicht authentifizierte Anfrage einer Ressouce, die eine Authentifizerung erfordert
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example"

Der Client muss dann einen HTTP-Service-Endpunkt aufrufen, der eine Authentifizierung ermöglicht. Ein solcher Endpunkt zum Login kann mit der Klasse BearerLoginHandler wie in folgendem Beispiel implementiert werden.

Beispiel für einen Service-Endpunkt zur Authentifizierung
// einen Login Handler erstellen und mit dem Authenticator verbinden
HttpContext context = server.createContext("/meine-app/login", new BearerLoginHandler());
context.getAttributes().put(BearerLoginHandler.ATTR_AUTHENTICATOR, auth);

Über den Kontext /meine-app/login kann eine Benutzeranmeldung erfolgen. Der BearerLoginHandler erwartet den Aufruf per HTTP POST mit {"name": "fred", "password": "secret"} im Body der Anfrage. Zudem wird dem Kontext über das Attribut BearerLoginHandler.ATTR_AUTHENTICATOR mitgegeben, welcher Authenticator beim Login verwendet werden soll.

Der Login Handler überprüft Nutzerangaben gegen den Realm des Authenticators und gibt bei erfolgreicher Authentifizierung den Authentifizerungs-Token und den Refresh-Token zurück. Diese benötigt der Client zur Angabe der Authentifizierung im Header von Anfragen an Ressourcen, die eine Authentifizierung erfordern.

Die Authentifizierungsangaben werden mit Hilfe der Klasse LoginResponse im spezifikationsgemäßen Format als Ausdruck in JavaScript Object Notation (JSON) zurückgegeben.

die Antwort auf eine erfolgreiche Authentifizierung
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"mF_9.B5f-4.1JqM",
  "token_type":"Bearer",
  "expires_in":3600,
  "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA"
}

BearerRefreshHandler

Ist ein Authentifizierungs-Token abgelaufen, erhält der Client bei Anfrage einer Ressource, die eine Authentifizierung erfordert, eine Ablehnung.

Antwort auf die Anfrage einer Ressource, die eine Authentifzierung erfordert, wenn die Authentifizierung nicht mehr gültig ist
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example",
                    error="invalid_token",
                    error_description="The access token expired"

Bei der Bearer Authentication muss der Client den Refresh Token aus der letzten Authentifizierung verwenden, um die Authentifizierung zu erneuern. Mit der Klasse BearerRefreshHandler liefert http-oauth das Mittel zur Bildung eines Service-Endpunkts, der einen Token erneuert.

Beispiel für einen Service-Endpunkt zum Erneuern einer Authentifizierung
HttpContext context = server.createContext("/meine-app/refresh", new BearerRefreshHandler());
context.getAttributes().put(BearerLoginHandler.ATTR_AUTHENTICATOR, auth);

Der Instanz von BearerRefreshHandler wird wie beim Login Handler wieder der Authenticator mitgegeben, mit dessen Hilfe Refresh Tokens geprüft werden. Mit Aufruf des Endpunkts /meine-app/refresh erwartet der BearerRefreshHandler eine HTTP-Anfrage im spezifikationsgemäßen Format wie folgt:

Anfrage zum Erneuern einer Authentifizierung
POST /meine-app/refresh HTTP/1.1
Host: example.com

grant_type=refresh_token
&refresh_token=xxxxxxxxxxx
&client_id=xxxxxxxxxx
&client_secret=xxxxxxxxxx

Ist der Refresh Token gültig, antwortet der BearerRefreshHandler so wie beim Login mit einem neuen Satz Tokens.

Zusammenfassung

Mit dem Modul http-oauth kann eine App oder ein Microservice den Bearer Authentication Mechanismus für Ressourcen verwenden, wenn diese nur an berechtigte Nutzer ausgegeben werden sollen.

  1. Zum Einschalten der Authentifizierung wird ein Service-Endpunkt mit einem Objekt der Klasse BearerAuthenticator verbunden.

  2. Mit einem Objekt der Klasse BearerLoginHandler wird ein Service-Endpunkt zur Authentifizerung geschaffen.

  3. Ein Objekt der Klasse BearerRefreshHandler erlaubt die Herstellung eines Service-Endpunktes zum Erneuern einer abgelaufenen Authentifizierung.

Damit ist für eine App oder einen Microservice eine Authentifizierung herstellbar, die stärker von etablierten Mechanismen wie Basic, Digest oder Form Based Authentication entkoppelt ist und zudem flexibler in evtl. bestehende andere Authentifizierungsdienste eingebunden werden kann.

http-up

Das Modul http-up stellt die Klasse MultipartStream aus dem Projekt Apache Commons mit geringfügigen Anpassungen bereit, mit denen die Klasse ohne weitere Abhängigkeiten eingesetzt werden kann. Das folgende Code-Beispiel zeigt, wie die Klasse auf der Server-Seite z.B. in einem HttpHandler verwendet werden kann, um zum Server heraufgeladene Dateien entgegenzunehmen.

Beispiel für den Einsatz der Klasse MultipartStream
public static final String ATTR_FILENAME = "filename=";
public static final String TEMP_FILE_NAME = "temp.up";

private String fileBase = "/some/path/to/storage";

public void handle(HttpExchange exchange) throws IOException {
    Headers headers = exchange.getRequestHeaders();
    String ct = headers.getFirst(HttpHelper.CONTENT_TYPE);
    String[] parts = ct.split("=");
    String boundary = parts[1];
    InputStream is = exchange.getRequestBody();
    MultipartStream multipartStream = new MultipartStream(is, boundary.getBytes(), 4096, null);
    File file = new File(fileBase, TEMP_FILE_NAME);
    try {
      String value = "";
      boolean nextPart = multipartStream.skipPreamble();
      while (nextPart) {
        String header = multipartStream.readHeaders();
        if(header.contains(ATTR_FILENAME)) {
          // process file content
          // perhaps insert some file name processing here too
          // so that more than one uploaded files do not overwrite
          // each other
          OutputStream os = new FileOutputStream(file);
          multipartStream.readBodyData(os);
        } else {
          // read value
          ByteArrayOutputStream os = new ByteArrayOutputStream();
          multipartStream.readBodyData(os);
          value = os.toString().substring(fileContext.length());
        }
        nextPart = multipartStream.readBoundary();
      }
    } catch (MultipartStream.MalformedStreamException e) {
      // the stream failed to follow required syntax
    } catch (IOException e) {
      // a read or write error occurred
    }

    // processing of files 'temp.up', 'temp-2.up', 'temp-3.up' etc.
    // is required here, e.g. rename, move ..
}

Die Methode handle im obigen Beispiel liest mit Hilfe der Klasse MultipartStream eine HTTP-Anfrage mit Content-Type multipart/form-data, wie sie bei HTTP-Uploads an den Server gesendet wird.

http-adoc

Mit http-adoc wird Neon um die Möglichkeit erweitert, Dokumente im Asciidoctor-Format nach HTML oder PDF zu transformieren. Neben der Datei http-adoc.jar sind die Java-Archive aus dem Paket AsciidoctorJ erforderlich. Die Umwandlung geschieht 'on-demand', es muss kein besonderer Build-Prozess hergestellt werden. Die Aktivierung des AdocFilter für eine Ressource wie nachfolgend beschrieben genügt.

Beispiel für die Aktivierung von AdocHandler und AdocFilter
// einen Descriptor fuer den AdocHandler erstellen
HandlerDescriptor hd = new HandlerDescriptor();
hd.setHandlerClassName("de.uhilger.httpserver.adoc.AdocHandler");

// einen regulären Ausdruck fuer das Muster *.adoc erstellen
// und den AdocHandler daran binden
PatternDelegator handler = new PatternDelegator();
handler.addHandler(".+\\.adoc", hd);

// einen neuen HttpContext erzeugen
HttpContext context = server.createContext("/meine-app/dok", handler);

// den AdocFilter fuer den neuen HttpContext aktivieren
context.getFilters().add(new AdocFilter());

// den Pfad zu den Daten setzen
context.getAttributes.put(FileHandler.ATTR_FILE_BASE, "/home/fred/www-daten");

Im obigen Code-Beispiel werden alle Inhalte des Ordners /home/fred/www-daten über den Service-Endpunkt /meine-app/dok via HTTP ausgegeben. Zudem wird der AdocFilter für alle über /meine-app/dok zugängliche Inhalte eingeschaltet.

Beispiel

Beim Aufruf der Ressource http://localhost:9191/meine-app/dok/hilfe/anleitung.adoc wird mit obiger Konfiguration die Datei /home/fred/www-daten/hilfe/anleitung.adoc nötigenfalls nach HTML transformiert, als anleitung.html am selben Ort gespeichert und diese HTML-Datei ausgeliefert.

Bleibt die Quelldatei anleitung.adoc unverändert, wird bei weiteren Aufrufen der Ressource die HTML-Datei unverändert ausgeliefert. Nach Änderungen an der Quelldatei erfolgt vor der ersten Auslieferung zunächst automatisch wieder eine Transformation.

Klassen

AdocFilter

Der Filter prüft für jede HTTP-Anfrage, ob der Name der gewünschten Ressource mit .adoc endet. Wenn ja, wird geprüft, ob bereits eine HTML-Version der Datei existiert. Wenn nicht oder wenn die HTML-Version älter ist als die letzte Änderung der .adoc-Datei veranlasst der AdocFilter den AdocActor eine neue HTML-Version zu erzeugen.

AdocHandler

Die Klasse AdocHandler dient zum 'Einklinken' der Funktion in die HttpHandler-Logik des Moduls jdk.httpserver. Der AdocHandler leitet Anfragen an Ressourcen mit Endung .adoc um auf die Auslieferung ihrer HTML-Version.

AdocActor

Die eigentliche Transformation erledigt die Klasse AdocActor, die dafür die Funktionen von AsciidoctorJ nutzt. Der AdocActor kann so losgelöst von Neon oder den Strukturen von jdk.httpserver auch für andere Programme verwendet werden, wenn sie eine solche Transformation benötigen.

Übrigens: Die Klassen HandlerDescriptor, PatternDelegator und FileHandler sind im Modul http-base enthalten, das für die allermeisten Verwendungen von Neon vermutlich ohnehin immer eingebunden wird.

http-image

Das Modul http-image erzeugt verkleinerte Fassungen eines Originalbildes 'on demand'. Hierzu wird die Klasse ImageFilter einem HttpContext hinzugefügt. Neben http-image.jar sind die Klassenbibliotheken http-base sowie Thumbnailator im Classpath erforderlich.

Beispiel für die Aktivierung des ImageFilter
// einen HttpContext mit einem FileHandler erzeugen
HttpContext context = server.createContext("/meine-app/dateien", new FileHandler());

// den Ablageort von Inhalten angeben
context.getAttributes().put(FileHandler.ATTR_FILE_BASE, "/home/fred/www-daten"));

// den ImageFilter für den Context aktivieren
context.getFilters().add(new ImageFilter());

Damit wird automatisch für alle Dateien, die mit jpg, jpeg oder png enden, der ImageFilter aktiv. Liegt unter /home/fred/www-daten beispielsweise die Bilddatei mein-bild.jpg, wird mit Aufruf von http://example.com/meine-app/dateien/mein-bild_tn.jpg eine verkleinerte Fassung mit 120 Bildpunkten erzeugt. Die folgenden Bildgrößen werden dabei verarbeitet

Table 1. Vom ImageFilter erzeugte Bildgrößen
Bildpunkte Dateiname

120

_tn

240

_kl

500

_sm

700

_mt

1200

_gr

Desweiteren bewirkt der Zusatz _b64 die Erzeugung einer Base64-kodierten Fassung, die für Grafiken mit Data-URI dienen kann. Der Zusatz _b64 wird mit den obigen Namenszusätzen kombiniert indem er ihnen angehängt wird. Der Dateiname mein-bild_tn_b64.jpg erzeugt eine Base64-kodierte und auf 120 Bildpunkte verkleinerte Fassung des Originalbildes namens mein-bild.jpg.

http-cm

Beschreibung folgt..

http-template

Beschreibung folgt..