Aus Linux-Magazin 08/2011

Perl-Skript nutzt neue HTML-5-Features

© picsfive, 123RF.com

HTML 5 bringt Websockets, über die Webserver mit ihren Clients in einen Dialog treten können. Die im Folgenden vorgestellte kleine Webapplikation zeigt in Echtzeit im Browser, welche Seiten beliebige User von einem Webserver im Moment aufrufen.

Statt des simplen Frage-Antwort-Spiels im traditionellen HTTP-Protokoll versetzen die im HTML-5-Standard enthaltenen Websockets jeden neueren Standardbrowser in die Lage, über persistente Verbindungen bidirektional mit dem Webserver zu kommunizieren.

Dazu nimmt Javascript-Code auf einer heruntergeladenen Seite im Browser eine Websocket-Verbindung zur URL »ws://Server/Pfad« auf und definiert einen Callback, den der Code jedes Mal sofort anspringt, sobald der Server eine Nachricht über den Socket sendet. Die Browserapplikation reagiert damit sofort auf Signale des Servers, ohne ihn in regelmäßigen Abständen pollen zu müssen.

Kann’s der Browser?

Ob ein Browser Websockets unterstützt, lässt sich in Javascript leicht feststellen: Nur falls das DOM-Element »window.WebSocket« existiert, sind Websockets implementiert und aktiviert. Die Webseite Websocket.org bietet auf [2] eine kleine Applikation, die die Browserfertigkeiten mit grüner oder oranger Farbe anzeigt und mit einer kleinen Echo-Applikation zum Spielen anregt. Die Abbildungen 1 und 2 zeigen Firefox 4 erst mit deaktiviertem, dann mit aktiviertem Websocket-Modus.

Abbildung 2: Aktiviert der User die im Abschnitt "Sicherheitslücken" weiter unten erklärte Konfiguration, nutzt Firefox 4 das Websocket-API nach dem alten Protokoll.

Abbildung 2: Aktiviert der User die im Abschnitt “Sicherheitslücken” weiter unten erklärte Konfiguration, nutzt Firefox 4 das Websocket-API nach dem alten Protokoll.

Abbildung 1: Websocket.org bestätigt, dass Firefox 4 ohne »network.websocket.enabled« nicht über Websockets kommunizieren kann. Hier ist also erst ein Eingriff des Benutzers erforderlich, um die fragliche Fähigkeit zu aktivieren.

Abbildung 1: Websocket.org bestätigt, dass Firefox 4 ohne »network.websocket.enabled« nicht über Websockets kommunizieren kann. Hier ist also erst ein Eingriff des Benutzers erforderlich, um die fragliche Fähigkeit zu aktivieren.

Derzeit unterstützen nur Firefox 4 und Google Chrome das Protokoll zumindest eingeschränkt (siehe Abschnitt “Sicherheitslücken” weiter unten), für eine vollständige Implementierung nach dem neuesten Standard muss es sogar Firefox 6 (Aurora) sein.

Websockets im Test

Abbildung 3 zeigt eine Testapplikation, bei der der Browser in unregelmäßigen Abständen absteigende Zählerwerte vom Server empfängt. Der Server zählt dabei von 10 an abwärts, schickt den jeweiligen Zählerstand über einen Websocket zur dargestellten Browserseite und schläft einen per »rand()« gesteuerten zufälligen Sekundenbruchteil lang bis zum nächsten Schleifendurchgang. Bei Null angelangt, sendet der Server statt einer Ziffer die Zeichenkette »BOOM!« und beendet sofort darauf die Websocket-Kommunikation. Im Browser erscheinen die Nachrichten des Servers verzögerungsfrei und in Echtzeit, an die Applikation ausgeliefert über einen effizienten Callback-Mechanismus, der funktioniert, ohne dass der Client jedes Mal um Nachschlag bitten müsste.

Abbildung 3: Das Websocket-Testskript zählt einen ruckelnden Countdown.

Abbildung 3: Das Websocket-Testskript zählt einen ruckelnden Countdown.

Zum Implementieren des Testservers greift Listing 1 auf das bereits im vorigen Snapshot vorgestellte Framework Mojolicious::Lite zu, mit dem experimentierfreudige Programmierer in Minuten ganze Webapplikationen zusammenklopfen können. Es unterstützt neben normalen Webprotokollen auch Websockets und hält den Zustand jedes Websocket-Clients mittels einer Closure im Speicher.

Listing 1

cntdwn-random

01 #!/usr/local/bin/perl -w
02 use strict;
03 use Mojolicious::Lite;
04 use Mojo::IOLoop;
05
06 my $listen = "http://localhost:8083";
07 @ARGV = (qw(daemon --listen), $listen);
08
09 my $loop = Mojo::IOLoop->singleton();
10
11 ###########################################
12 websocket "/myws" => sub {
13 ###########################################
14   my($self) = @_;
15
16   my $timer_cb;
17   my $counter = 10;
18
19   $timer_cb = sub {
20     $self->send_message( "$counter" );
21     if( $counter-- > 0 ) {
22         $loop->timer(rand(1), $timer_cb);
23     } else {
24         $self->send_message( "BOOM!" );
25     }
26   };
27
28   $timer_cb->();
29 };
30
31 ###########################################
32 get '/' => sub {
33 ###########################################
34   my ($self) = @_;
35
36   ( my $ws_url = $listen ) =~ s/^http/ws/;
37   $ws_url .= "/myws";
38   $self->{stash}->{ws_url} = $ws_url;
39 } => 'index';
40
41 app->start();
42
43 __DATA__
44 @@ index.html.ep
45 % layout 'default';
46 Random counter:
47 <font size=+2 id="counter"></font>
48
49 @@ layouts/default.html.ep
50 <!doctype html><html>
51   <head><title>Random Countdown</title>
52     <script type="text/Javascript">
53       var socket = new WebSocket(
54                      "<%== $ws_url %>" );
55       socket.onmessage = function (msg) {
56         document.getElementById(
57           "counter").innerHTML = msg.data;
58       };
59     </script>
60   </head>
61   <body> <%== content %> </body>
62 </html>

Die Eventschleife Mojo::IOLoop bietet einer Applikation ein Timer-gesteuertes Hook-Framework, um in einem laufenden Mojolicious-Prozess hin und wieder die Kontrolle zu erlangen, kleine Aufgaben zu erledigen und das nächste Update zu planen.

Statt eines Konstruktors »new()« nutzt Zeile 9 die Methode »singleton()« , die eine weitere Referenz auf eine Eventschleife liefert, falls vorher schon eine definiert war. Das ist wichtig, denn verschiedene Eventschleifen würden jeweils ihre Vorgänger auslöschen.

Perpetuum Mobile

Im Testskript definiert der Callback »$timer_cb« ab Zeile 19 (Listing 1) eine Funktion, die den globalen Countdown-Wert mit der Mojo-Methode »send_message()« über den Websocket zum Browser sendet und ihn danach um eins herunterzählt. Nach getaner Arbeit ruft Zeile 22 die Methode »timer()« des Moduls Mojo::IOLoop auf, das die Anzahl der zu schlafenden Sekunden und eine Callback-Funktion entgegennimmt.

Einen Fließkommawert zwischen 0 und 1 liefert »rand(1)« , also wacht die Eventschleife nach weniger als einer Sekunde wieder auf und springt die gleiche in »$timer_cb« gespeicherte Callback-Funktion an (Abbildung 4). Um den Reigen anfangs in Schwung zu bringen, ruft Zeile 28 einmalig den Callback auf, der sich ab dann selbstständig fortsetzt.

Abbildung 4: Nachdem der Client einen Websocket mit dem Server eröffnet hat, kann der Server asynchron Daten an den Client schicken.

Abbildung 4: Nachdem der Client einen Websocket mit dem Server eröffnet hat, kann der Server asynchron Daten an den Client schicken.

Den Ansprungspunkt im Server bei eingehenden Websocket-Requests definiert das Kommando »websocket« in Zeile 12, im Gegensatz zu »get« in Zeile 32, das die Reaktion des Testservers auf »GET« -Anfragen zum Rootpfad »/« des Webservers beantwortet. Der Websocket-Handler rechnet die gegebene »http://« -URL mit einem regulären Ausdruck in eine »ws://« -URL für Websocket-Requests um, fügt den Pfad »/myws« an und stopft die Gesamt-URL unter dem Schlüssel »ws_url« in den Template-Automaten. Zeile 39 verweist mit »index« auf das weiter unten im »__DATA__« -Bereich definierte Template »@@ index.html.ep« .

Der zugehörige HTML-Code enthält etwas Text (»Random counter« ) sowie ein Font-Element mit der ID »counter« . Zwar springen CSS-Puristen jetzt im Dreieck, aber schließlich geht es nur darum, ein HTML-Element mit einer bekannten ID zu definieren, damit der Javascript-Code es später aus der DOM herausfischen und den Inhalt auffrischen kann.

Bitte melden

Das damit verknüpfte Layout »default« ab Zeile 49 macht ein ordentliches HTML-Dokument aus dem oberen HTML-Schnipsel und fügt Javascript-Code zur Websocket-Ansteuerung hinzu. Zeile 53 erzeugt ein neues Objekt der Klasse »WebSocket« unter Verwendung der vorher erzeugten »ws://« -URL. Nimmt der Websocket Kontakt zum Server auf und absolviert den Handshake, bewirkt eine später vom Server an den Browser über den Websocket geschickte Nachricht ein Event namens »socket.onmessage« , dessen Callback-Funktion Zeile 55 setzt.

In dem Attribut »data« der übersandten Nachricht steht der Textstring, den »send_message()« auf der Server-Seite am anderen Ende der Rohrpost vorher eingetütet hat. In diesem Beispiel ist das also der aktuelle Zählerwert im Countdown, Zeile 56 braucht nur das DOM-Element mit der ID »counter« zu suchen und dessen Attribut »innerHTML« auf den Zählerwert zu setzen, um Letzteren zur Anzeige zu bringen. Geht der User mit dem Browser auf die in Zeile 6 gesetzte URL »http://localhost:8032« , fängt der Countdown an zu zuckeln.

Als praktische Applikation sieht Listing 2 aktiven Usern einer Webseite über die Schulter und zeigt in einem Fenster in Echtzeit die Seiten an, die auch die Surfer zu Gesicht bekommen, zusammen mit der URL und dem anzeigenden Host (Abbildung 6). Der Kontrolleur braucht nur den Mojolicious-Server in der URL-Zeile des Browsers anzugeben – und schon frischt dieser in Echtzeit die Anzeige mit den gerade ausgelieferten Webseiten auf. Wie ist das möglich?

Listing 2

apache-peek

001 #!/usr/local/bin/perl -w
002 ###########################################
003 # apache-peek
004 # Mike Schilli, 2011 (m@perlmeister.com)
005 ###########################################
006 use strict;
007 use Mojolicious::Lite;
008 use ApacheLog::Parser
009     qw(parse_line_to_hash);
010 use Mojo::IOLoop;
011 use POSIX;
012 use Socket;
013 use Json qw(encode_Json);
014
015 my $listen = "http://website.com:8083";
016 @ARGV = (qw(daemon --listen), $listen);
017
018 my $base_url = "http://website.com";
019
020 my $file = "access.log";
021 sysopen my $fh, "$file",
022     O_NONBLOCK|O_RDONLY or die $!;
023
024 my $loop = Mojo::IOLoop->singleton();
025
026 ###########################################
027 websocket "/myws" => sub {
028 ###########################################
029   my($self) = @_;
030
031   my $timer_cb;
032   $timer_cb = sub {
033     for my $line ( @{ tail( $fh ) } ) {
034       my %fields =
035         parse_line_to_hash( $line );
036
037       if( $fields{ request } eq "GET" and
038           $fields{ file } =~ /\.html?$/ and
039             # skip our own requests
040           $fields{ client } ne
041             $self->tx->remote_address
042         ) {
043         my $url = $base_url .
044                   $fields{ file };
045         my $data = {
046           url  => $url,
047           host =>
048             revlookup( $fields{ client } ),
049         };
050         $self->send_message(
051             encode_Json( $data ) );
052         last;
053       }
054     }
055     $loop->timer( 5, $timer_cb);
056   };
057   $timer_cb->();
058 };
059
060 ###########################################
061 get '/' => sub {
062 ###########################################
063   my ($self) = @_;
064
065   (my $ws_url = $listen) =~ s/http/ws/;
066   $ws_url .= "/myws";
067   $self->{stash}->{ws_url} = $ws_url;
068 } => 'index';
069
070 app->start();
071
072 ###########################################
073 sub tail {
074 ###########################################
075   my($fh) = @_;
076
077   my($buf, $chunk, $result);
078
079   while( $result = sysread $fh,
080                            $chunk, 1024 ) {
081     $buf .= $chunk;
082   }
083
084   if( defined $result and defined $buf) {
085     chomp $buf;
086     my @lines = map { s/\s+$//g; $_; }
087                 split /\n/, $buf;
088     return \@lines;
089   }
090
091   return [];
092 }
093
094 ###########################################
095 sub revlookup {
096 ###########################################
097   my($ip) = @_;
098
099   my $host = (gethostbyaddr(
100           inet_aton($ip), AF_INET ))[0];
101   $host = $ip unless defined $host;
102   return $host;
103 }
104
105 __DATA__
106 @@ index.html.ep
107 % layout 'default';
108
109 Host: <em id="host"></em>
110 URL: <em id="url"></em>
111
112 <iframe width=100% height=800 src=""
113         id="pageview"></iframe>
114
115 @@ layouts/default.html.ep
116 <!doctype html><html>
117   <head><title>Apache Peek</title>
118     <script type="text/Javascript">
119       var socket = new WebSocket(
120                      "<%== $ws_url %>" );
121       socket.onmessage = function (msg) {
122         var data = eval( "(" +
123                           msg.data + ")" );
124         document.getElementById(
125           "host").innerHTML = data.host;
126         document.getElementById(
127           "url").innerHTML = data.url;
128         document.getElementById(
129           "pageview").setAttribute( "src",
130                                 data.url );
131       };
132     </script>
133   </head>
134   <body> <%== content %> </body>
135 </html>

So funktioniert derSurfer-Kiebitz

Die Funktion »tail()« ab Zeile 73 in Listing 2 sieht einmal pro Sekunde nach, ob der »sysread« -Befehl auf ein im »O_NONBLOCK« -Modus geöffnetes »access.log« des Webservers neue Daten zutage fördert. Findet das Skript damit »GET« -Requests wie in Abbildung 5, die auf eine HTML-Seite zeigen und nicht von der IP-Adresse des Skripts selbst stammen, verpackt es die angeforderte URL zusammen mit der in einen Hostnamen verwandelten IP-Adresse des Anfragenden in ein Json-Konstrukt und schickt es in Zeile 51 zum Websocket des Browsers.

Abbildung 5: Ein Request im Logfile des Webservers.

Abbildung 5: Ein Request im Logfile des Webservers.

Der Callback »socket.onmessage« entpackt in Zeile 121 das Json-Format, indem er es in runde Klammern einschließt und mit Javascripts »eval()« -Funktion ausführt. Er sucht die HTML-Elemente mit den IDs »host« , »url« und »pageview« im HTML-Code (Template ab Zeile 106) und packt die aus Json extrahierten Daten in die entsprechenden Anzeigefelder.

Das hat zur Folge, dass die im Browser angezeigte HTML-Seite bei jedem eintreffenden Request für eine HTML-Seite auf dem Webserver den in ihr eingebetteten »iframe« (Zeile 112) mit der in der Logdatei gefundenen URL aktualisiert, also dem Kontrolleur die Webseite anzeigt, die auch der Surfer sieht. Gleichzeitig aktualisiert das Skript am oberen Seitenrand noch die angeforderten URL mitsamt dem aufgelösten Hostnamen des anfragenden Surfers.

Abbildung 6: Im Schaufenster zeigt der Surfer-Kiebitz an, welcher Benutzer gerade welche Webseite ansteuert.

Abbildung 6: Im Schaufenster zeigt der Surfer-Kiebitz an, welcher Benutzer gerade welche Webseite ansteuert.

Mit der blitzschnellen asynchronen, vom Server initiierten Aktualisierung lässt sich so in Echtzeit verfolgen, wer was auf dem Webserver ansieht. Damit das Skript bei prasselnden Anfragen nicht ins Schlingern gerät, beschränkt Zeile 55 die Untersuchung des Access-Log auf einmal alle 5 Sekunden und nimmt jeweils nur den ersten Treffer, der den Anforderungen in den Zeilen 37 bis 39 genügt: Nur »GET« -Requests, um ungewollte Replays auf datenverändernde »POSTs« zu unterbinden, nur HTML-Seiten (falls jemand andere Endungen als ».html?« nutzt, muss er dies anpassen) und keine Requests, die von der IP des Kontrolleurs selbst kommen. Der Browser frischt die gerade angezeigten URLs nur alle 5 Sekunden auf, egal wie schnell die Client-Requests ankommen.

Die in Zeile 35 aufgerufene Funktion »parse_line_to_hash()« analysiert die ihr überreichte Access-Log-Zeile und wandelt sie in einen Hash mit den Schlüsseln »client« (Client-IP), »file« (angeforderter Dateipfad in der URL) und so weiter um. »revlookup()« ab Zeile 95 transformiert eine IP über Reverse-DNS in einen Hostnamen, behält aber die ursprüngliche IP bei, falls dies fehlschlägt.

Sicherheitslücken

Die Websockets-Implementierung in Firefox 4 und Google Chrome basiert auf der Draft-Version 76 des Protokolls, die noch ernste Sicherheitsmängel aufweist. Zwar treten diese nur bei unverschlüsselter Kommunikation und fehlerhaft programmierten Proxys auf, doch im Fall des Falles wäre der Enduser im realen Internet unter Umständen böswilligen Attacken ausgesetzt.

Die aktuelle Version von Mojolicious auf dem CPAN (1.42) unterstützt deswegen nur die korrigierte Version des Protokolls, basierend auf der IETF-08-Spezifikation. Firefox 4 oder Google Chrome können mit dieser allerdings nichts anfangen, doch Firefox 6 (Aurora) weiß damit umzugehen. Wer allerdings mit älteren Browsern testen möchte, der kann die alte Mojolicious-Version 1.16 vom CPAN laden, die damals nach dem Draft 76 des Websocket-Protokolls programmiert wurde. Für Produktionssysteme ist davon jedoch wegen der Sicherheitsmängel dringend abzuraten.

Firefox 4 deaktiviert seine Websockets wegen der veralteten Implementierung deshalb von Haus aus. Nur wenn ein waghalsiger Teufelskerl im Dialog »about:config« die Boolean-Variablen »network.websocket.enabled« und »network.websocket.override-security-block« auf »true« setzt, schaltet Firefox 4 das Feature frei (Abbildungen 7 und 8).

Abbildung 7: Ein neuer Firefox-Eintrag in »about:config« aktiviert das Websocket-API.

Abbildung 7: Ein neuer Firefox-Eintrag in »about:config« aktiviert das Websocket-API.

Abbildung 8: Wegen Sicherheitsbedenken muss der Anwender Websockets im Firefox 4 explizit aktivieren.

Abbildung 8: Wegen Sicherheitsbedenken muss der Anwender Websockets im Firefox 4 explizit aktivieren.

Die erforderlichen CPAN-Module Mojolicious, ApacheLog:: Parser und Json lassen sich leicht mit einer CPAN-Shell installieren. Das Modul ApacheLog::Parser bereitet allerdings einige Schwierigkeiten bei der Installation, da es seinerseits auf dem Modul Time::Piece beruht, dessen Testsuite fehlschlägt. Der Grund dafür ist ein seit rund einem Jahr bestehender Bug im Zusammenspiel mit Test::Harness, der zwar der Funktion des Moduls keinen Abbruch tut, aber seine per CPAN-Shell ausgeführten Tests scheitern lässt. Am besten bedient sich der Anwender hier des Kommandos »force install« , das zur erfolgreichen Installation führt.

Online PLUS

In einem Screencast demonstriert Michael Schilli das Beispiel unter: https://www.linux-magazin.de/plus/2011/08

Ausblick

Websockets stecken sicherlich noch in den Kinderschuhen, es wird wohl noch dauern, bis alle gängigen Browser auch die sichere Protokollversion verwenden. Doch das Potenzial für Browserapplikationen, die ihren Verwandten vom Desktop ähneln, ist enorm. Besonders im Chat- oder Videobereich lassen sich praktische Anwendungen denken.

Infos

  1. Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2011/08/Perl
  2. Websocket-Testseite:http://websocket.org/echo.html
  3. Jamie Popkin, “Watch Your Processes Remotely with Mojolicious and a Smartphone”: Linux Journal, 2011/07, S. 58
  4. Stefan Neufeind, “Webstecker”: iX 06/2011, S. 60

Der Autor

Michael Schilli arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. Er hat “Goto Perl 5” (auf Deutsch) und “Perl Power” (auf Englisch) für Addison-Wesley geschrieben und ist unter mailto:mschilli@perlmeister.com zu erreichen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben