Über die Same Origin Policy und den Preflight zur Access Control
Da es zu diesem Thema kaum deutschspachige Dokumentationen gibt, soll dieser kleine Artikel eine Lücke füllen. Kurzum: AJAX
-Requests sind domänenübergreifend möglich.
Ohne zusätzliche Custom-Header im Request funktioniert CORS
mit den genannten Request-Methoden auf Anhieb unter einer Bedingung: Die Response enthält einen speziellen Header Access-Control-Allow-Origin: *
, betrachte ein Beispiel, zunächst der JavaScript-Code im Browser einer beliebigen Dömäne:
var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(){ if(xhr.readyState == 4 && xhr.status == 200){ alert(xhr.response); } }; xhr.open("POST", 'http://rolfrost/cgi-bin/up.cgi', true); xhr.send('WORD');
Serverseitig wird Folgendes gesendet, Beispiel in Perl:
#!/usr/bin/perl use strict; use warnings; use HTTP::Headers; print header( 'Content-Type' => 'text/plain; Charset=UTF-8', 'Access-Control-Allow-Origin' => '*', ), do{ read(STDIN, my $buffer, $ENV{CONTENT_LENGTH}); "Gesendete Daten: $buffer"; }; sub header{ my $self = HTTP::Headers->new(@_); return $self->as_string()."\n"; }
Anstelle des Asterisk
(Origin
beliebig) kann im Header auch der zugelassene Origin notiert sein, Beispiel 'Access-Control-Allow-Origin' => 'http://example.com'
.
Sofern ein von o.g. Request-Methoden (HEAD, GET, POST
) abweichender Request erfolgen soll, etwa ein PUT
oder wenn im Request Custom-Header
mitgesendert werden sollen, findet ein sogenannter Preflight statt, d.h., das XHR
-Objekt sendet zunächst und spontan (selbstständig) einen Request mit der Methode OPTIONS
, um die Zugänglichkeit der Ressource zu erkunden. Einmal angenommen, der Custom-Header sei wie untenstehend und gewünscht wird ein PUT
:
xhr.open("PUT", 'http://rolfrost/cgi-bin/up.cgi', true); xhr.setRequestHeader('x-request', 'ajax'); xhr.send('WORD');
Ein etwas ausführlicherer serverseitiger Code:
if($ENV{REQUEST_METHOD} eq 'OPTIONS' || $ENV{REQUEST_METHOD} eq 'PUT' || $ENV{REQUEST_METHOD} eq 'POST'){ print header( 'Content-Type' => 'text/plain; Charset=UTF-8', 'Access-Control-Allow-Origin' => '*', 'Access-Control-Allow-Methods' => 'POST, GET, OPTIONS, PUT', 'Access-Control-Allow-Headers' => 'x-request', ), do{ read(STDIN, my $buffer, $ENV{CONTENT_LENGTH}); "Gesendete Daten: $buffer"; }; }
Statt einem, erfolgen nun zwei Requests, wofür das XHR
-Objekt sorgt:
OPTIONS /cgi-bin/up.cgi HTTP/1.1
PUT /cgi-bin/up.cgi HTTP/1.1
wie in der Mozilla-Netzwerk-Konsole zu sehen ist. Die übertragenen Daten werden bei einem PUT
-Request serverseitig aus STDIN
gelesen, genauso wie bei einem POST
. Zweckmäßigerweise wird im Fall OPTIONS
die Response mit Content-Length: 0
und ohne Daten gesendet, in obenstehendem Code wird dies jedoch nicht weiter unterschieden, der Code dient nur zur Veranschaulichung. Wichtig ist, dass die bezüglich Access-Control
relevanten Response-Header nicht nur beim Preflight gesendet werden müssen, sondern auch in der Response zum Request, mit welchem die eigentliche Datenübertragung erfolgt.
Jeder XHR
-Request sendet einen Origin-Header:
Origin: http://example.org ^ Authority ^ Scheme
Womit der Begriff Origin
verständlich werden dürfte. Die Same Origin Policy
(SOP
) sorgt dafür, dass XHR
-Requests gewöhnlich nur dann möglich sind, wenn Origin
beim Sender mit Origin
des Empfängers übereinstimmt.
Diese Request-Methode ist nicht neu. Sie diente schon immer dazu, dass ein UserAgent
beim HTTP-Server anfragen kann, welche Request-Methoden erlaubt sind, Beispiel:
# Request OPTIONS / HTTP/1.1 Host: example.org # Response Allow: GET, HEAD, POST
Für XHR
-Requests bekommt OPTIONS
eine neue Bedeutung, XHR
kennt die Response-Header:
Access-Control-Allow-Origin Access-Control-Allow-Methods Access-Control-Allow-Headers
und sendet diesbezüglich im OPTIONS
-Request:
Access-Control-Request-Headers Access-Control-Request-Method Origin
Wobei nach dem Aushandeln der Policy mit Request-Method OPTIONS
beim darauffolgenden PUT
-Request die Access-Control-Request-[Headers|Method]
-Header nicht noch einmal gesendet werden. Im zweiten Request verbleiben jedoch der Origin
-Header und ggf. die X-Custom
-Headers. Ein OPTIONS
-Request sollte keine Daten senden, dies ist in XMLHttpRequest()
so implementiert, dass der OPTIONS
-Request spontan erfolgt (hierzu sendet das XHR
-Objekt nur die Header).
Pro Request kann es nur eine Methode geben, hinzu kommt der Enctype
, welcher den Request-Header Content-Type
ergibt. Ein RFC
-gerechter Parser unterscheidet zunächst, welche Request-Method vorliegt. Parameter gibt es nur für den Enctype multipart/form-data
oder application/x-www-form-urlencoded
, auch das muss beim Parsen der in den gesendeten Daten enthaltenen Parametern berücksichtigt werden.
Des Weiteren sind Parameter nur mit der Request-Method POST
oder GET
zu erwarten, ein PUT
-Request hingegen kennt keine Parameter und auch keinen Enctype
. Liegt ein POST
vor, liest der Parser die Daten aus STDIN
, bei einem GET
liest der Parser die Daten aus dem QUERY_STRING
. Ein RFC
-gerechter Parser liefert also entweder POST
- oder GET
-Daten, eben weil es entweder ein POST
- oder ein GET
-Request ist. Auf jeden Fall verhält sich der in CGI.pm
implementierte Parser auf diese Art und Weise.
Da hier, wie bereits angemerkt, keine Parameter zu erwarten sind, erfolgt die serverseitige Kontrolle in einem diesem Sachverhalt entsprechenden Programm-Abschnitt beispielsweise so:
if( $ENV{REQUEST_METHOD} eq 'OPTIONS'){ # ggf. eine eigene Prüfung des Origin print header( 'Content-Type' => 'text/plain; Charset=UTF-8', 'Access-Control-Allow-Origin' => '*', 'Access-Control-Allow-Methods' => 'POST, GET, OPTIONS, PUT', 'Access-Control-Allow-Headers' => 'x-request', 'Content-Length' => 0 ); } elsif( $ENV{REQUEST_METHOD} eq 'PUT' ){ # Rohdaten aus STDIN lesen # $Response erstellen print header( 'Content-Type' => 'text/plain; Charset=UTF-8', 'Access-Control-Allow-Origin' => '*', 'Access-Control-Allow-Methods' => 'POST, GET, OPTIONS, PUT', 'Access-Control-Allow-Headers' => 'x-request', ), $Response; } else{} # Request-Method not implemented
Hinweis zur Verwendung CGI.pm: Nur bei einem von multipart/form-data
oder application/x-www-form-urlencoded
abweichenden Enctype
sind die Rohdaten in param('POSTDATA')
oder param('PUTDATA')
zu finden. Ansonsten liefert param('Parameter_Name')
den zum Parameter gehörigen Wert bzw. ein Array
der Werte bei mehreren gleichnamigen Parametern im Request.
Da bei einem PUT
-Request ohnehin nur Rohdaten (Binaries) übertragen werden und der Enctype
nicht näher bestimmt ist, empfiehlt es sich, auf den Einsatz von CGI.pm
zu verzichten und die Binaries selbst aus STDIN
zu lesen.
Beispiel für eine Anwendung.
Datenschutzerklärung: Diese Seite dient rein privaten Zwecken. Auf den für diese Domäne installierten Seiten werden grundsätzlich keine personenbezogenen Daten erhoben. Das Loggen der Zugriffe mit Ihrer Remote Adresse erfolgt beim Provider soweit das technisch erforderlich ist. sos@rolfrost.de.