IPv4 1.2.3.4

Als ich kürzlich die Zeilen

Waiting for 1.2.3.4

und dann noch

Transferring from 1.2.3.4

in der Status-Zeile des Firefox Browser las wurde ich etwas unrund. Die aufgerufene Seite (http://snowhow.info) hatte ich selbst entwickelt und ganz sicher keinen Link auf http://1.2.3.4 eingebaut. Irgendwie schaute die Seite auch anders aus, aber nicht wesentlich. Also den Quellcode der Seite studiert und da steht es: Vor der head-Anweisung hat jemand Javascript-Code aus einer anderen Domain eingeschleust:

<script src="http://1.2.3.4/bmi-int-js/bmi.js" language="javascript"></script>

Gehackt

Die Seite wurde gehackt, ganz klar. Aber wie konnte ich gehackt werden und wer hat eigentlich die extrem coole IP-Adresse 1.2.3.4 auf die da verlinkt wird? Dr. Google hilft in solchen Fällen ja immer sehr zuverlässig: In meinem Fall wird das Javascript von meinem aktuellen Internet Service Provider (eine Prepaid SIM von Vodafone Greece) eingefügt um Datenvolumen zu sparen. Also nicht gehackt, zuerst einmal durchschnaufen.

Doch nicht gehackt

Wie viele Seiten im Netz zeigen ist es gar nicht einfach (oder mir zumindest nicht gelungen) die wohl gut-gemeinte Optimierung abzuschalten: Vodafone proxyt alle Bilder, verkleinert sie massiv und schickt nur schlecht aufgelöste Versionen an den Client. Für Entwickler unangenehm dabei ist, dass der Javascript-Code nicht gerade state-of-the-art ist: Es werden einige globale Variable erzeugt (oder überschrieben) und es wird versucht eine Art Tooltip mit inkorrekten Informationen über alle Bilder zu legen.

Und wer ist jetzt 1.2.3.4?

Wie Google ebenfalls ans Licht brachte wurde der Adressblock im Jahr 2010 zwar vergeben, aber er kann nicht seriös verwendet werden, weil Skripts wie dieses den Bereich vollständig überschwemmen. Schade eigentlich.

Fazit

Mit HTTPS wäre das nicht passiert! Die Übertragung der Webseite wird dabei vom Browser zum Webserver verschlüsselt, was es für den ISP unmöglich macht eine Javascript Datei unterzuschieben.

Nodejs und WordPress mit gemeinsamer Authentifizierung

Das Problem

Bei snowhow verwenden wir WordPress als Content-Management-System. Die Benutzer-Accounts für die snowhow User werden auch in WordPress verwaltet. Warum? Das System ist sehr weit verbreitet, die User-Verwaltung sollte also ausgereift sein (ge-hashte Passwörter, Passwort per E-Mail zurücksetzen).

Außerhalb von WordPress als CMS setzen wir auf Nodejs und MongoDB. Die aufgezeichneten Freeride-Tracks und Skitouren kommen als GeoJSON direkt in die MongoDB und können dort via räumlicher Suche gefunden werden, herrlich unkompliziert. Aber wie kann der Nodejs-Server (express) auf die Authentifizierung von WordPress zugreifen?

  • Das node-Modul node-wordpress-auth funktionierte mit einer älteren Version von WordPress, war aber immer schon ein dirty hack.
  • Die Passport Authentifizierung via OAuth2 für WordPress wäre prinzipiell möglich (ein OAuth2-Provider-Plugin für WordPress vorausgesetzt), die Nuss wäre damit wohl mit dem Vorschlaghammer geknackt.
  • Als Lösung wählten wir den gemeinsamen Zugriff auf Session-Daten von PHP und Nodejs (hier kommt uns zugute, dass der Nodejs-Server durch den apache, der WordPress betreibt, ge-proxy-t wird). Wie das geht?

Die Lösung: Gemeinsamer Zugriff auf Session Daten

Klingt einfacher, als es dann war. Hier die Kurzversion:

  1. Die PHP Sessions in den memcached Speicher verlegen.
  2. Ein WordPress Plugin schreiben, das die User-ID in die Session speichert.
  3. Nodejs Module installieren: memcached cookies php-unserialize.
  4. Den Javascript Code anpassen, damit die Session-Daten ausgelesen und dekodiert werden können.

Im Detail sind dazu folgende Schritte notwendig:
ad 1.) Zuerst wird der memcached-Server installiert. Unter Ubuntu Linux ein leichtes mit

apt-get install memcached php5-memcached

Der Server startet (in der Standardeinstellung unter Ubuntu) automatisch auf Port 11211 und der Zugriff ist nur vom localhost möglich. Als nächstes gilt es die PHP Konfiguration anzupassen. Im Fall von Ubuntu legt man zum Beispiel die Datei /etc/php5/apache2/conf.d/30-memcached-sessions.ini mit folgendem Inhalt an:

session.save_handler = memcached
session.save_path='127.0.0.1:11211'

Jetzt nicht vergessen den apache neu zu starten, dann sollten die Sessions im memcached Server liegen.

ad 2.) WordPress verwaltet die Session in einem eigenen Cache, nicht in der PHP Session. Ein kleines Plugin bringt die Login Informationen von WordPress in die PHP Session (die Idee dazu stammt von dieser Seite):

add_action('init', 'startSession', 1);
add_action('wp_logout', 'endSession');
add_action('wp_login', 'setSessionData', 10, 2);

function startSession() {
  session_name("snowhowSession");
  if(!session_id()) {
    session_start();
    $_SESSION['snowhowinit'] = true;
  }
}

function setSessionData($user_login, $user) {
  $_SESSION['wp_user_id'] = $user->ID;
  $_SESSION['wp_user_email'] = $user->user_email;
}

function endSession() {
  session_destroy();
}

ad 3.) Das sollte ein Leichtes sein:

npm install memcached cookies php-unserialize

Wird der Javascript Code in Form eines Node-Pakets verwaltet (was vermutlich eine gute Idee ist), trägt man die Abhängigkeiten am besten gleich in die package.json Datei ein und kann sie am Produktionssystem mit npm install installieren.

ad 4.) Zum Schluss wird noch eine Javascript-Funktion definiert, die die Login Informationen, sofern vorhanden, zurück gibt:

var Cookies = require('cookies');
var PHPUnserialize = require('php-unserialize');
var Memcached = require('memcached');
var mem = new Memcached('127.0.0.1:11211');

function getWPUser(req, res, cb) {
  var cookies = new Cookies(req, res);
  var sessionCookie = "memc.sess.key."+cookies.get("snowhowSession");
  mem.get(sessionCookie, function(err,data) {
    var user;
    if (err) cb({ error: err });
    if (data) {
      try {
        user = PHPUnserialize.unserializeSession(data);
      } catch(e) {
        user = {};
      }
    }
    cb(user);
  });
}

Hier ist noch anzumerken, dass das php-unserialize Modul nicht korrekt mit PHP Objekten (new stdClass) umgehen kann (zumindest nicht bei meiner Installation).

Fazit

Einige Klimmzüge sind notwendig, um die zwei Serverdienste auf der Backendseite zu verbinden. Das Session-Cookie (hier mit dem Namen snowhowSession) ist der gemeinsame Nenner der beiden Dienste. Das System funktioniert natürlich nur, wenn der Nodejs-Server durch den apache ge-proxy-t wird.

Cordova/Phonegap Plugin als Android Service

Das Problem

App-Entwicklung mit Cordova funktioniert großartig. Beim Aufzeichnen von GPS Tracks stößt man aber an die Grenzen der Technologie, da es keine Möglichkeit gibt, einen Prozess zu starten, der nicht automatisch vom System beendet werden kann. Dazu benötigt man ein Plugin…

wpid-screenshot_2014-07-30-10-13-30.png

Ein Android Service

Mit ein wenig Java Kenntnissen ist es einfach einen Hintergrund-Dienst (Service) als Plugin für Cordova zu realisieren. Android kann den Service zwar auch beenden, startet ihn aber sofort wieder, wodurch das GPS tracking fortgesetzt wird. Einen Haken gibt es bei der Sache allerdings: Die Kommunikation zwischen Plugin und der Web-app ist schwierig. Die vorhandene Schnittstelle über den Callback-Context funkioniert (nach meinem Wissen) nicht, da der Service abgekoppelt läuft.

GPS Websocket Server

Moderne Browser unterstützen das effiziente Websocket Protokoll nativ. Der GPS-Service startet einen einfachen Websocket-Server auf einem hohen Port und die Web-app verbindet und wartet auf Messages:

    var ws = new WebSocket('ws://localhost:8887/snowhow');
    ws.onmessage = function (evt) {
      var data;
      try {
        data = JSON.parse(evt.data);
      } catch (e) {
        console.log("illegal json data via websocket", evt.data);
      }
      handleWSResopnse(data);
    }

Doch damit nicht genug: Über den Websocket Server kann das HTML-UI auch die Nachricht zum beenden des Service schicken. Natürlich steigt durch den Server und den Websocket Client der Stromverbrauch etwas, erste Tests haben aber keine dramatischen Änderungen gezeigt.

HTTP requests aus Javascript auf der Entwicklermaschine

Ein Problem, das vermutlich mehrere Entwickler haben, die in einer Web-Applikation mit Javascript auf ein backend zugreifen: Entwicklet wird auf der lokalen Maschine, das backend liegt (zum Entwickeln) auf einem anderen Server.

Browser lassen HTTP-Requests in Javascript von http://localhost/ auf eine andere Domain nicht zu (hier gilt die same origin policy). Auf den Seiten des Mozilla Developer Network wird sehr übersichtlich erklärt, wie man diese Einschränkung sicher umgehen kann. Da wir im content der requests json verschicken müssten unsere Zugriffe recht aufwändig als preflighted requests behandelt werden.

Bisher verwendeten wir zur Umgehung dieses Problems google-chrome mit dem Command-line switch --disable-web-security. Etwas unschön und vor allem konnten wir mit Firefox erst testen, nachdem der Code am Entwicklungsserver deployed war.

Mit wenigen Zeilen in der lokalen apache Konfiguration ist das Problem auch vom Tisch: Man richtet einen lokalen Proxy ein, der die ajax-requests an den Server weitergibt:

  <IfModule mod_proxy.c>
    ## mod_proxy for ajax CORS troubles
    ProxyPassMatch ^/(.*/specimen/.*)$ http://server.tld/$1
    ProxyPassMatch ^/(.*/j_spring_security_logout)$ http://server.tld/$1
    ProxyPassMatch ^/(.*/j_spring_security_check)$ http://server.tld/$1
  </IfModule>

Ist das apache-Modul mod_proxy aktiviert (a2enmod mod_proxy), werden alle requests die in der Adresse den Pfad /specimen/ enthalten zu server.tld umgeleitet. Gleiches gilt für die security-checks.

Von jqplot Diagrammen zu einer PDF Datei

ich sags gleich, ein steiniger Weg.

Die Anforderung

Zum Zweck der analogen Archivierung musste ich für ein aktuelles Projekt (CHES) eine moderne HTML-Seite (mit mehreren Canvas-Elementen) in eine druckbare Form bringen. Mit CSS-Anweisungen ist es mir nicht gelungen, nur in zwei modernen Browsern ein halbwegs vergleichbares Layout zu Papier zu bringen. Mit viel Javascript ging es eher in diese Richtung, aber im dritten Browser war dann auch da wieder alles anders.

Eine PDF-Datei war die schlüssige Entscheidung. Diese kann dann auch digital archiviert werden, was bei CHES, einem Projekt im Medizinumfeld, durchaus sinnvoll scheint. Jetzt wäre der einfachste Weg folgender: Verwenden Sie Google-Chrome, drücken Sie auf diesen Button und wählen Sie im anschließenden Dialog „Speichern als PDF“. Aber das wäre ja zu einfach …

Das Ausgangsmaterial

CHES Longitudinal Report demo

Mit serverseitig-erzeugten PDFs habe ich schon mehrfach Erfahrung: mit PHP, wkhtmltopdf, und batik gab es immer brauchbare Ergebnisse. Bei CHES verwenden wir die jqplot-Bibliothek von Chris Leonello, die (mehr oder weniger) aufwändige Diagramme mit Hilfe von HTML und canvas im Browser anzeigt. Die Darstellung kann clientseitig angepasst werden, soweit so gut. Serverseitig kann ich diese Bibliothek aber nicht verwenden und um die veränderte Seite durch wkhtmltopdf zu jagen, müsste ich alle Veränderungen mitgeben und … ach, zu kompliziert. Also versuchen wir das PDF am Client zu erzeugen: jspdf.com.

jspdf.com

jspdf ist noch ein recht junges Projekt und vom Funktionsumfang überschaubar. Für die Ansprüche im aktuellen Projekt schien es möglich, einen Versuch damit zu starten. Mit canvas kann jspdf (noch) nicht umgehen, darum werden die Diagramme zuerst in Grafiken konvertiert. Der Qualitätsverlust ist schmerzlich, wo jspdf obendrein nur mit JPEGs und nicht mit PNG arbeiten will.

  var doc = new jsPDF();
  $(jquery_selector_fuer_jqplots_hier).each(function() {
    // hier werden xoff, yoff, w, h berechnet
    // jqplot muss ge-patcht werden, damit nicht image/png sondern image/jpeg kommt
    var img = $(this).jqplotToImageStr();
    doc.addImage(img, 'JPEG', xoff, yoff, w, h);
  });
  doc.save("ches_report_"+what+"_"+CHES.patientWithoutPrefix+".pdf");

Und hier das Resultat, not too bad (Firefox, Linux):

CHES Report PDF mit jspdf

Wunderbar, und sehr übersichtlicher Code. Funktioniert auch in Firefox, Chrome, Safari (sogar am iPad), you name it. Ach ja, der IE, da geht das aller leider nicht. Mehrere Problem:

  • IE8: Kennt das canvas Element nicht. Damit man im IE8 überhaupt jqplot Diagramme sehen kann lädt man die excanvas Bibliothek. Alles wunderbar, sehr langsam, aber immerhin, 100% Funktionalität. Fast, die toDataURL()-Methode ist nicht unterstützt und nach meinen ausführlichen google-Recherchen wird das im IE8 auch nie möglich sein. Die Folge: Der IE8 kann diese Seiten weder drucken noch in PDFs konvertieren.
  • IE9: Hmm, pdfjs funktioniert wunderbar, allein das PDF kann weder angezeigt noch gespeichert werden. Klingt absurd, ist aber so. Der workaround ist noch aberwitziger: Man lade eine kleine Flash-Bibliothek, die einen Button anzeigt, der den PDF-String herunterladen lässt. Unglaublich aber wahr, hier der Code (vollständiges Beispiel bei github.com):
        if (badIE === true) {
          var docString = doc.output('datauristring');
          $("#ieWarning").show();
            Downloadify.create('downloadify',{
              filename: "ches_report_"+what+"_"+CHES.patientWithoutPrefix+".pdf",
              data: docString,
              onComplete: function(){ $("#ieWarning").hide(); },
              onCancel: function(){ $("#ieWarning").hide();  },
              onError: function(){ $("#ieWarning").hide(); },
              dataType: 'base64',
              transparent: false,
              swf: 'js/jspdf/libs/Downloadify/media/downloadify.swf',
              downloadImage: 'js/jspdf/libs/Downloadify/images/download.png',
              width: 100,
              height: 30,
              append: false
            });
        }
    
  • IE10: soll das alles ohne schmutzige Tricks können, konnte ich aber noch nicht testen.

tl;dr — Fazit

Das sehr einfache PDF-Dokument kann mit jspdf gut erstellt werden. Der Ausschluss des IE8 ist mit der mangelnden Unterstützung des canvas-Elements begründet und nicht mit jspdf an sich. Der IE9 workaround ist aberwitzig, aber kann vermutlich bald entfernt werden (der IE9-Marktanteil ist eh rückläufig). Alle anderen, aktuellen Browser können gut bedient werden und erzeugen eine druckbare PDF Datei, die plattformübergreifend gleich aussieht.