CORS Cross-Origin Resource Sharing (deutsch)

Ü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.

HEAD, GET, POST

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'.

Preflighted Requests

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";
    };

}

Ablauf der Kommunikation

Statt einem, erfolgen nun zwei Requests, wofür das XHR-Objekt sorgt:

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.

Ergänzende Hinweise

Origin -> Policy

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.

OPTIONS -> Access-Control

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).

Serverseitige Kontrollstrukturen

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.

PUT-Request mit vorangestelltem OPTIONS-Request (Preflight)

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.


Anbieter: nmq​rstx-18­@yahoo.de, die Seite verwendet funktionsbedingt einen Session-Cookie und ist Bestandteil meines nach modernen Aspekten in Perl entwickelten Frameworks.