Apr 262008
 

Überaschenderweise finden Java Applets auch heute noch immer wieder ihre Nische in Systemlandschaften. Wenn es darum geht, aus einer Web-Anwendung heraus eine Schnittstelle zum lokalen System des Anwenders zu etablieren, sind Applets keine schlechte Wahl. Über die HTML-Seite in welche das Applet eingebunden ist, können Parameter an das Applet übergeben werden. Ebenso ist die bidirektionale Kommunikation zwischen Applet und Javascript möglich (siehe Netscape Live Connect). Ein signiertes Applet erlaubt es, lokale Dateien zu lesen und zu schreiben. Ebenso kann ein signiertes Applet mit entfernten Servern kommunizieren, obwohl es nicht von diesen geladen wurde.

Diese Möglichkeiten sind der Grund für den Einsatz von Applets in meinem aktuellen Projekt in München. Bei einem großen deutschen Automobilhersteller sind Applets ein (sehr kleiner) Teil derSystemlandschaft zum Produktdatenmanagement. Das nur am Rande.

Unter dem Strich erlauben wir dem Anwender mittels eines Applets Dateien zu und vom Server zu laden. Es handelt sich dabei um Office-Dokumente, Messergebnisdaten und CAD-Dateien. Die Dateien können also recht groß – gar bis über einem Gigabyte groß – sein.

Speicherproblem beim Versenden großer Datenmengen über HTTP

Die Übertragung der Dateien zum Server ist unter Verwendung der JDK-Klasse HttpURLConnection realisiert. Bei sehr großen Dateien wurde auffällig, dass die JVM sehr viel Speicher allokiert und irgendwann mit einen OutOfMemoryError die Ausführung des Applets das zeitliche segnet. Die Ursache war schnell gefunden: Die HttpURLConnection puffert sämtliche in einem HTTP-Post-Request zu übermittelnden Daten um die Anzahl der zu sendenden Bytes zu ermitteln. Diese Angabe muss die Implementierung von HttpURLConnection im Request-Header-Feld “content-length” übermitteln. Der Header muss vor den Nutzdaten beim HTTP-POST-Request gesendet werden. Die zu sendende Datei wird zwar mittels InputStream gelesen und direkt in den OutputStream von HttpURLConnection geschrieben, aber HttpURLConnection puffert die gesamte Datei im Hauptspeicher bevor auch nur ein Byte an den Ziel-HTTP-Server gesendet wird. Dies führt natürlich unweigerlich zu Problemen. Leider bietet die API von HttpURLConnection keine Möglichkeit die Content-Length zu setzen, so dass die zu versendenden Daten nicht gepuffert werden.


Lösung unter Java 5

Ab Java 5 muss die Implementierung der abstrakten Klasse HttpURLConnection den HTTP Chunked-Stream unterstützen, welche Teil der HTTP/1.1-Spezifikation (in 3.6 Transfer Encodings) ist. Die Java-API von HttpURLConnection unterstützt ab Java 5 die Methode “setChunkedStreamingMode”, die als Argument die Größe des Chunks in Bytes verlangt. Im Chunked-Modus wird der Inhalt einer HTTP-Anfrage bzw. einer HTTP-Antwort in Chunks also Stücken versendet. Es muss nur noch die Länge jedes einzelnen Chunks im Header des Chunks übermittelt werden. Daher puffert das HttpURLConnection-Objekt in diesem Modus nicht mehr die gesamte Datei sondern nur noch jeweils einen Chunk. Für Java 5 ist damit das OutOfMemory-Problem gelöst.

Lösung unter Java 1.4

Das war natürlich viel zu einfach. Unterhalb von Java 5 ist es nicht möglich, das Transfer-Encoding “Chunked” mit der HttpURLConnection zu verwenden. Die HttpURLConnection wird die zu sendenden Daten immer erst vollständig vor dem Versenden puffern. Daher kann basierend auf der HttpURLConnection auch nicht der Chunked-Modus selbst implementiert werden.

Um mit Java 1.4 den Chunked-Modus zu verwenden wurde Apache Commons HttpClient verwendet. Dieser unterstützt den besagten Chunked-Modus. Die HttpClient-Bibliothek setzt direkt auf Java-Sockets auf und verwendet die HttpURLConnection nicht. Soweit so gut, wenn da nicht das Problem mit den Cookies wäre…

Cookies für Siteminder Authentifizierung

Das Applet sendet Dateien an ein Servlet, welches die Daten schlussendlich physisch ablegt. Der Zugriff auf das Servlet ist mittels Authentifizierungsmechanismus über die proprietäre Single-Sign-On Lösung Siteminder geschützt. Der Anwender meldet sich per Login am Unternehmensportal an und kann fortan auf verschiedene Web-Applikationen im Verbund zugreifen. Beim Portal-Login sendet das Portal ein entsprechendes Session-Cookie an den Browser zurück. Solange der Browser nicht geschlossen wird oder das Cookie abläuft, übermittelt der Browser bei jedem HTTP-Request an eine Web-Applikationen des Portal-Verbunds dieses Session-Cookie.

Siteminder verwendet ein Cookie mit dem Name “SMSESSION”, welches – wie der Name schon sagt – die ID der Siteminder-Sitzung als Wert beinhaltet. Dadurch wird das Single-Sign-On (SSO) erreicht. Aus diesem Grund sollte Siteminder wie auch andere Authentifizierungsmechanismen nur in Verbindung mit HTTPS (SSL / TLS) verwendet werden um Angrifffe nach dem Schema “man in the middle” zu verhindern

Damit das Applet beim Senden der Dateien an das Servlet die Authentifizierung passieren kann, muss es beim HTTP-Request das Cookie mit der SM-Session-ID übertragen. Wird die HttpURLConnection für die HTTP-Kommunikation verwendet, so werden alle Cookies an das Ziel übermittelt, die auch der Browser senden würde. Welche der im Browser verfügbaren Cookies gesendet werden ist lediglich von der Cookie-Domain und dem Cookie-Pfad abhängig. Die Cookie-Domain legt fest, an welche Server das Cookie gesendet wird. Der Cookie-Pfad legt fest, für welche Ressourcen auf einem Server das Cookie bestimmt ist. Der Cookie-Pfad “/” definiert das Senden des Cookies für alle Ressourcen auf dem Server.

Die relevante Implementierung der abstrakten Klasse HttpURLConnection von Java 1.4 befindet sich im “plugin.jar” – also einem Teil des Java-Plugins für Web-Browser. Eine Analyse durch Dekompilierung zeigt, dass die Klasse “sun.plugin.net.cookie.PluginCookieManager” verwendet wird um die Cookies aus dem Browser zu ermitteln. PluginCookieManager verwendet dafür wiederum eine Implementierung des Interface “CookieHandler” um. Es scheint Implementierungen der Schnittstelle “CookieHandler” für verschiedene Browser (IE, Netscape) zu geben, welche jedoch durch native Methoden realisiert sind.

Cookies und Commons HttpClient

Commons HttpClient gibt sich keine Mühe um Cookies vom Browser zu erhalten. Darum müssen wir uns selbst kümmern. In Java 1.4 ist es uns möglich auf den PluginCookieManager selbst zuzugreifen, um die Cookies vom Browser zu erhalten. Die Klasse PluginCookieManager existiert ab Java 5 nicht mehr unter diesem Namen. Ab Java 5 existiert mit java.net.CookieHandler eine Standard-konforme Möglichkeit zum Auslesen der Cookies. Da der Zugriff auf PluginCookieManager proprietär für Java 1.4 ist, erfolgt er im folgenden Code per Reflection um NoClassDefFoundErrors in höheren Java5-Versionen zu vermeiden:

private static String getCookiesFromPluginCookieManager(URL pUrl) {
  final String errMsg = "could not get cookies java 1.4 style";
  Class classCookieManager;
  try {
    classCookieManager = Class.forName("sun.plugin.net.cookie.PluginCookieManager");
  }
  catch (ClassNotFoundException e) {
    classCookieManager = null;
    log.info(errMsg, e);
  }
  catch (ExceptionInInitializerError e) {
    classCookieManager = null;
    log.info(errMsg, e);
  }

  if (classCookieManager == null) {
    return null;
  }

  Method methodGetCookieInfo;
  try {
    methodGetCookieInfo = classCookieManager.getMethod("getCookieInfo", new Class[] { URL.class });
  }
  catch (NoSuchMethodException e) {
    methodGetCookieInfo = null;
    log.info(errMsg, e);
  }
  catch (SecurityException e) {
    methodGetCookieInfo = null;
    log.info(errMsg, e);
  }

  if (methodGetCookieInfo == null) {
    return null;
  }

  Object ret;
  try {
    ret = methodGetCookieInfo.invoke(null, new Object[] { pUrl });
  }
  catch (IllegalArgumentException e) {
    log.info(errMsg, e);
    ret = null;
  }
  catch (IllegalAccessException e) {
    log.info(errMsg, e);
    ret = null;
  }
  catch (InvocationTargetException e) {
    log.info(errMsg, e);
    ret = null;
  }

  String cookies;
  if (ret instanceof String) {
    cookies = (String) ret;
    log.info("got cookies from PluginCookieManager: " + cookies);
  }
  else {
    cookies = null;
    log.info("got no string cookies from PluginCookieManager");
  }

  return cookies;
}

Die Cookies kommen als einzelner String separiert durch Semikolon vom PluginCookieManager zurück. Jeder Cookie besteht aus Name und Wert. Um die Cookies in Commons HttpClient setzen zu können müssen aus den Teilen der Cookie-Zeichenkette einzelne Cookie-Objekte erstellt werden. Dies kann durch folgenden Code erledigt werden:

private static Collection createCookieNameAndValuePairs(String cookies) {
      StringTokenizer stok = new StringTokenizer(cookies, ";");
      Collection cookieNameAndValuePairs = new ArrayList();
      while (stok.hasMoreTokens()) {
        cookieNameAndValuePairs.add(stok.nextToken());
      }
      return cookieNameAndValuePairs;
    }

private static Cookie[] createCookies(String pCookieDomain, Collection pCookieNameValuePairs) {
  if (pCookieNameValuePairs == null) {
    return new Cookie[0];
  }

  List cookies = new ArrayList();
  for (Iterator itCookies = pCookieNameValuePairs.iterator(); itCookies.hasNext();) {
    String nameAndValuePair = (String) itCookies.next();

    if (nameAndValuePair != null) {
      int posEq = nameAndValuePair.indexOf("=");
      if (posEq > 0) {
        String name = nameAndValuePair.substring(0, posEq).trim();
        String value = ((posEq + 1 < nameAndValuePair.length()) ? nameAndValuePair.substring(posEq + 1)
          : "").trim();

        if (name.length() > 0) {
          cookies.add(new Cookie(pCookieDomain, name, value, "/", null, false));
        }
      }
    }
  }

Damit Commons HttpClient die Cookies auch tatsächlich beim Request sendet, muss beim Aufruf des Konstruktors von Cookie auch die korrekte Cookie-Domain (zum Bsp. der DNS-Name des Ziel-Servers) und das Gültigkeitsdatum (null für unbeschränkte Gültigkeit) angegeben werden. Dem HttpClient-Objekt können dann die Cookies mitgeteilt werden:

private static void addCookies(HttpClient client, Cookie[] cookieAr) {
    if (cookieAr != null && cookieAr.length > 0) {
      // Get initial state object
      HttpState initialState = new HttpState();

      for (int i = 0; i < cookieAr.length; i++) {
        initialState.addCookie(cookieAr[i]);
      }
      client.setState(initialState);
    }

    client.getParams().setCookiePolicy(CookiePolicy.RFC_2109);
  }

Per Default wird HttpClient jeden Cookie einzeln als HTTP-Header-Feld “Cookie” an den Server übermitteln. Leider hat damit das Siteminder-Modul für den Apache HTTP-Server (zumindest in der vom Kunden eingesetzten Version) ein Problem. Im Log des Siteminder-Moduls findet sich “Unable to process SMSESSION cookie”. Das Siteminder-Modul erwartet sämtliche Cookies separiert per Semikolon in einem Wert eines einzigen Header-Parameters “Cookie”. Um Commons HttpClient das Setzen der Cookies als einen einzelnen Header-Parameter zu befehligen, ist folgender Code-Schnipsel nötig:

client.getParams().setParameter("http.protocol.single-cookie-header", Boolean.TRUE);

Wechselnde ID der Siteminder-Sitzung

Die Siteminder Session-ID wird von Siteminder immer wieder verändert. Dies soll zusätzliche Sicherheit verprechen: Eine veraltete Session-ID kann nicht mehr für HTTP-Requests verwendet werden. Ändert sich die Sitzungs-ID so wird Siteminder als Antwort auf einen HTTP-Request ein Header-Feld mit Namen “Set-Cookie” übermitteln, in welchem der neue Wert des Cookies mit Namen “SMSESSION” hinterlegt ist.

Daher verwendet das Applet nur ein einziges HttpClient-Objekt um alle HTTP-Requests auszuführen (Hochladen mehrerer Dateien). Damit ist sichergestellt, dass das HttpClient-Objekt bei Änderung der Sitzungs-Id das Cookie “SMSESSION” aktualisiert und beim nächsten HTTP-Request diesen aktualisierten Wert verwendet.

Die hier beschriebene Lösung lässt noch ein Problem außer Acht: Cookies die der Server als Antwort an den HttpClient zurücksendet, werden nicht an den Browser propagiert. Dies kann jedoch falls notwendig über “PluginCookieManager.setCookieInfo” realisiert werden. Dies ist notwendig, um die geänderte SM-Session-Id im Browser zu aktualisieren.

Fazit

Mit Java 5 wäre das nicht passiert. Die notwendigen Änderungen um das Speicherproblem bei großen Dateien im beschriebenen Fall zu lösen, hätte sich bei Java 5 auf eine Zeile beschränkt (Aufruf von “HttpURLConnection.setChunkedStreamingMode”). Mit Java 1.4 ergeben sich eine Reihe von Problemen, die auf den nicht unterstützten Transfer-Modus “Chunked” zurückzuführen sind.

Mit ist es nicht plausibel, warum große Konzerne bei der Umsetzung von sehr vielen Systemen auf Java setzen aber auf der anderen Seite eine Ewigkeit brauchen neue Versionen von Java flächendeckend auszurollen. Java 5 ist seit September 2004 verfügbar und bietet zahllose Verbesserungen, die zweifelsfrei zu Einsparungen und Fehlervermeidung bei der Umsetzung von Systemen beitragen. Dennoch brauchen einige Konzerne mehr als vier Jahre um sich zu einem Upgrade durchzuringen. Meiner Meinung nach wird damit viel Geld verbrannt.

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>