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.

