Durchbruch für Echtzeit-AJAX: XMLHttpRequest mit verschiedenen Servern

Posted by Carsten Bormann Tue, 29 Nov 2005 08:06:00 GMT

Will man AJAX für die Echtzeit-Darstellung von Informationen benutzen, so möchte man, daß der Client vom Server über jede relevante Zustandsänderung sofort informiert wird. Dafür eignet sich am besten eine HTTP-Verbindung, die vom Browser offengehalten wird, und in die der Server neue Daten einspeist, sobald diese verfügbar sind.

Dabei gibt es jedoch zwei Klassen von Problemen:

Serverseite

Die heute üblichen Web-Server wie Apache sind nicht sehr gut darin, viele gleichzeitig offene Verbindungen zu verwalten. Man möchte eigentlich die Echtzeit-Verbindungen trennen von den HTTP-Verbindungen, über die große Datenmengen geschaufelt werden. So verwendet S6 einen Synchronisationsserver, der mitteilt, wann ein neuer Zustand vorliegt — der Client kann dann (über eine separate Verbindung) Daten nachfordern. (Bei S6 ist dieses Nachfordern noch nicht in Benutzung, weil das zugrundeliegende System S5 die Präsentation vorweg als ganzes lädt, aber man kann sich S6 durchaus auch mit mehr AJAX-Funktionalität vorstellen.)

XMLHttpRequest läßt aber aus Sicherheitsgründen nur Zugriffe auf dieselbe Domain zu, von der auch die Seite selbst stammt — eine sinnvolle Erweiterung der same origin policy, der wichtigsten Grundlage der JavaScript-Sicherheit. Bei S6 bedeutet das, daß die inhaltsschweren HTTP-Zugriffe vom selben Server beantwortet werden müssen wie die Synchronisations-Zugriffe. Das ist auch der Grund für den etwas instabilen Aufbau des Demo-Servers:

Ein Webrick-Server unter http://tzi.org:2000/s6.html liefert sowohl Inhalte als auch Synchronisation. Webrick ist aber als Server für Inhalte nicht besonders effizient (außerdem belegt der Apache-Server den Port 80 auf tzi.org, so daß Webrick auf den wenig Firewall-freundlichen Port 2000 ausweichen mußte).

Als Alternative wird die S6-Demo deswegen unter http://www.tzi.org/~cabo/s6/s6.html auch über Apache angeboten. Im Proxy-Modus werden die Synchronisationsanfragen an den Webrick-Server weitergeleitet. Das bedeutet aber, daß jeder S6-Follower eine andauernde Verbindung zum Apache-Server aufrechterhält (was unser Webmaster zum Glück noch nicht recht wahrgenommen hat :-). Nicht skalierbar.

Clientseite

Selbst wenn bessere Proxy-Server das serverseitige Problem mit der durch die XMLHttpRequest-Sicherheit geforderten Serverbindung lösen würden, gibt es noch einen anderen Grund, Synchronisationsanfragen von Inhaltsanfragen trennen zu wollen:

Um die Web-Server nicht zu überlasten, haben Browser meist eine Begrenzung für die Anzahl der HTTP-Verbindungen, die gleichzeitig zu einem Server geöffnet werden — dieses Maximum liegt heute im allgemeinen bei zwei. Wird davon eine für Synchronisationsanfragen dauerhaft belegt, wird die andere zum Flaschenhals. (Und hat ein Client erst einmal zwei Echtzeit-Anwendungen vom selben Server offen, geht für den Client auf diesem Server gar nichts mehr.)

Auch hier wäre es also höchst sinnvoll, XMLHttpRequest-Anfragen auf mehrere Domänen verteilen zu können: Inhalte von www.tzi.org, Synchronisationsanfragen von sync.tzi.org. XMLHttpRequest weigert sich aber (aus guten Gründen) hartnäckig, Domain-übergreifende Anfragen zuzulassen. (Eine etwas offenere Sicherheitspolitik wird zwar gelegentlich diskutiert, aber das nützt natürlich nichts für die nächsten ein, zwei Jahre.)

Der Durchbruch

Abe Fettig, seines Zeichens Entwickler der interessanten Plattform JotSpot Live (mehr dazu in einem anderen Blogpost), beschrieb gestern abend seinen Ansatz, mit der JavaScript-Eigenschaft document.domain die beschriebenen Sicherheitseinschränkungen von XMLHttpRequest teilweise zu umgehen.

Die Eigenschaft document.domain stellt seit NetScape Navigator 2.0 eine Möglichkeit zur Verfügung, die same origin policy auf mehrere Server mit einer gemeinsamen Haupt-Domain auszudehnen: In unserem Fall (www.tzi.org und sync.tzi.org) würde man also document.domain auf tzi.org stellen, und dann sind Cross-Domain-Zugriffe möglich. Ganz so einfach ist es bei XMLHttpRequest allerdings nicht: Die Domain muß genau übereinstimmen!

Die grobe Idee von Fettig: Der JavaScript-Code mit dem XMLHttpRequest für den zweiten Server wird in einen iframe verfrachtet, der im Gegensatz zum Hauptdokument von dieser zweiten Domain (z.B. sync.tzi.org) geladen wird. Das Problem ist damit darauf reduziert, eine Kommunikation zwischen iframe und Hauptdokument (geladen z.B. von www.tzi.org) zu ermöglichen — und hier kommt uns document.domain zu Hilfe.

Der naheliegende Ansatz ist es, im iframe zunächst den XMLHttpRequest durchzuführen, dann document.domain auf die gleiche übergeordnete Domain zu stellen, die man vorher schon im Hauptdokument eingestellt hat (in unserem Fall wäre das tzi.org). Nach dieser Änderung kann man vom iframe aus auf das Hauptdokument zugreifen, um das Ergebnis mitzuteilen.

Problem: nachdem im iframe document.domain auf den Wert tzi.org umgesetzt ist, funktioniert natürlich kein weiteres XMLHttpRequest zu sync.tzi.org mehr. Mein Ansatz wäre nun wohl gewesen, für weitere Anfragen dann eben weitere iframes zu erzeugen.

Fettig hat hingegen einfach probiert, document.domain wieder auf den alten Wert (in unserem Beispiel wäre das sync.tzi.org) zurückzusetzen. Siehe da: In IE und Safari/Konqueror funktioniert das auch. In Mozilla/Firefox und Opera leider nicht (was ich für eine valide Interpretation der same origin policy halte), und da kommt schon der nächste Trick: Fettig hebelt die Kommunikationsbeschränkungen zwischen Hauptdokument und iframe einfach ein bißchen aus, indem er ein weiteres iframe dazwischenschiebt (ebenfalls aus sync.tzi.org geladen), das das eigentliche Kommunikations-iframe erzeugt, dort eine Variable installiert, die auf eine im Zwischen-iframe definierte Closure (JavaScript-Funktion) verweist. Im Zwischen-iframe kann dann document.domain auf tzi.org gesetzt werden, um mit dem Hauptdokument zu kommunizieren; das Kommunikations-iframe verbleibt in der Domain sync.tzi.org. Mozilla/Firefox (und nur dieser) hat nichts dagegen, und die Kommunikation flutscht über diesen kleinen Umweg. (Fettig hat noch keine Lösung für Opera, aber das ist ihm nicht so wichtig.) Jetzt brauchen wir nur noch eine Methode, um zwischen den beiden Lösungen umzuschalten: die erste wirft ja bei Mozilla/Firefox eine Ausnahme; die wird abgefangen und es wird auf die Mozilla-Alternative umgeschaltet.

Was für ein Hack, aber: Today’s “hack” is tomorrow’s “standard”.

Ein Durchbruch im doppelten Sinne

Ich würde mich bei der ganzen Sache wesentlich wohler fühlen, wenn die für Mozilla/Firefox erforderliche Kommunikation über die Zwischeninstanz mit dem Kommunikationskanal Closure sich nicht so sehr wie ein Sicherheitsloch anfühlen würde, das auch für bösere Zwecke genutzt werden kann und deswegen in zukünftigen Mozilla/Firefox-Versionen wieder geschlossen wird. Als nächster Schritt ist jetzt wohl eine Runde vernünftiger Security-Analysen angesagt.

Posted in  | Tags , , , ,  | 2 comments | 291 trackbacks