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.
-
http-base - Dateien und Streams ausliefern
-
http-realm - Nutzerverzeichnis zur Authentifizierung
-
http-oauth - Bearer Authentication für Neon
-
http-up - Dateien zu Neon heraufladen
-
http-adoc - Asciidoctor mit Neon transformieren
-
http-image - Bilder mit Neon verwenden
-
http-cm - Dateien mit Neon verwalten
-
http-template - Mustache-Vorlagen mit Neon rendern
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.
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
PatternDelegatorkönnenHttpHandleran 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 vonNeonHandlerabgeleitet, 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
HandlerDescriptorerlaubt im Zusammenspiel mit einemPatternDelegatordie 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.
SimpleRealmSimpleRealm 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.
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.
// 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.
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.
// 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.
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.
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.
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:
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.
-
Zum Einschalten der Authentifizierung wird ein Service-Endpunkt mit einem Objekt der Klasse
BearerAuthenticatorverbunden. -
Mit einem Objekt der Klasse
BearerLoginHandlerwird ein Service-Endpunkt zur Authentifizerung geschaffen. -
Ein Objekt der Klasse
BearerRefreshHandlererlaubt 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.
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.
// 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
.adocendet. 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 derAdocFilterdenAdocActoreine neue HTML-Version zu erzeugen. - AdocHandler
-
Die Klasse
AdocHandlerdient zum 'Einklinken' der Funktion in die HttpHandler-Logik des Modulsjdk.httpserver. DerAdocHandlerleitet Anfragen an Ressourcen mit Endung.adocum auf die Auslieferung ihrer HTML-Version. - AdocActor
-
Die eigentliche Transformation erledigt die Klasse
AdocActor, die dafür die Funktionen von AsciidoctorJ nutzt. DerAdocActorkann so losgelöst von Neon oder den Strukturen vonjdk.httpserverauch 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.
// 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
| Bildpunkte | Dateiname |
|---|---|
120 |
|
240 |
|
500 |
|
700 |
|
1200 |
|
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.