{"id":491,"date":"2010-01-21T04:36:32","date_gmt":"2010-01-21T03:36:32","guid":{"rendered":"http:\/\/blog.benny-baumann.de\/?p=491"},"modified":"2010-01-21T04:36:32","modified_gmt":"2010-01-21T03:36:32","slug":"cross-domain-xmlhttprequest-foo-mit-custom-http-methods","status":"publish","type":"post","link":"https:\/\/blog.benny-baumann.de\/?p=491","title":{"rendered":"Cross-Domain XmlHttpRequest-Foo mit Custom HTTP Methods"},"content":{"rendered":"<p>Kleine Vorwarnung vorweg: Es wird technisch! Wem die \u00dcberschrift nichts sagt, empfehle ich vor dem Fortsetzen in diesem Post das Studium <a href=\"http:\/\/www.w3.org\/TR\/access-control\/#rfc2616-field-name\">diverser Standards<\/a>. Zudem sei bereits hier angemerkt: Die hier vorgestellte Technik funktioniert auf Grund der Same Origin Policy nicht mit allen Browsern. Getestet (und als Funktionierend bekannt) ist aber mindestens alles, was FF 3.5.X oder besser hei\u00dft. Aber genug der Vorrede, fangen wir an.<!--more--><\/p>\n<p>Vor etwas l\u00e4ngerer Zeit bin ich auf einen Blogpost gesto\u00dfen, in dem erkl\u00e4rt wurde, wie man <a href=\"http:\/\/moggy.laceous.com\/2008\/08\/01\/custom-request-methods-and-php\/trackback\/\">Custom HTTP Methods mit PHP implementieren<\/a> kann. Nun gibt es daf\u00fcr eine Breite Verwendung, z.B. um eine RESTful API zu implementieren, aber so ganz allein auf dem eigenen Server macht dies i.d.R. wenig Sinn. Wohl aber, wenn man sich anschaut, dass GeSHi nur darauf wartet, als Webservice nutzbar zu sein &#8211; es gab sogar bereits Leute, die haben GeSHi in ASP und Java eingebunden. Also warum nicht auch GeSHi als REST-API?<\/p>\n<p>Also das Demo-Skript aus dem Blog-Eintrag herangezogen und auf dem Server eingerichtet. Funktionierte wie erwartet auf Anhieb, solange es unter Beachtung der Same-Origin-Policy seine Anfragen an das eigene Skript schickte. F\u00fcr einen so richtigen Webservice ist das aber eher weniger geeignet, wenn auch es auf Seiten von PHP mit relativ wenig Aufwand umsetzbar ist, eine Custom HTTP Method abzufangen (sofern der Server dies zul\u00e4sst):<\/p>\n<pre lang=\"php\">\r\n\/\/Check for Custom HTTP Method ...\r\nif (strtoupper($_SERVER['REQUEST_METHOD']) == 'MYMETHOD') {\r\n    \/\/Read RAW POST data ...\r\n    $raw_data = file_get_contents('php:\/\/input');\r\n\r\n    \/\/Split variables according to & signs ...\r\n    $array = explode('&', $raw_data);\r\n\r\n    \/\/Read POST Variables ...\r\n    foreach($array as $item) {\r\n        list($key, $value) = explode('=', $item, 2);\r\n        $_MYMETHOD[$key] = $value;\r\n    }\r\n\r\n    \/\/Do output generation here ...\r\n    echo \"Hello World!\";\r\n}\r\n<\/pre>\n<p>Wer bis hierhin durchgehalten hat, hat die H\u00e4lfte eigentlich schon geschafft, denn mit diesen wenigen Zeilen kann das Script bereits korrekt auf MYMETHOD-Anfragen reagieren. Doch nun zum interessanten Teil: XD-XHR.<\/p>\n<p>Normalerweise verhindert im Browser die Same-Origin-Policy die Ausf\u00fchrung von Abfragen, insbesondere bei Custom HTTP Methods \u00fcber Domaingrenzen hinweg. Seit dem Release von Firefox 3, gibt es aber, basierend auf einer Draft des W3C, die M\u00f6glichkeit, <a href=\"http:\/\/ajaxian.com\/archives\/cross-site-xmlhttprequest-in-firefox-3\/trackback\">diese Policy zu lockern<\/a>, wenn der empfangende Webserver sich bereiterkl\u00e4rt, den Request entgegen zu nehmen.<\/p>\n<p>Um diese Genehmigung zu erhalten sendet der Browser vor einem XHR einen sog. Preflight Request, in dem basierend auf einer Reihe von Headern dem Zielserver mitgeteilt wird, von welcher Quelldomain ausgehend, welche HTTP-Methode UND welche Resource verwendet werden soll. Basierend auf diesen Angaben muss der Zielserver nun eine Reihe von Headern generieren, um diesen Request zu akzeptieren. Im einfachsten Fall reicht hierff\u00fcr genau ein Header:<\/p>\n<pre lang=\"email\" escaped=\"true\">\r\nAccess-Control: allow &lt;*&gt;\r\n<\/pre>\n<p>Was aber aus Sicherheitstechnischen Gr\u00fcnden (World-Wide Accessible) nicht unbedingt zu empfehlen ist. Etwas vern\u00fcnftiger ist es da schon eher, basierend auf der Origin die im W3C-Draft vorgeschlagenen Header halbwegs vollst\u00e4ndig zu interpretieren UND zu generieren. Mit etwas Feingef\u00fchl kann man dabei allein anhand der Access-Control-Header bereits Request-Limiting und andere Funktionalit\u00e4t vom Client erfragen. Ein i.d.R. guter Anfang ist mit folgenden Headern gegeben:<\/p>\n<pre lang=\"email\" escaped=\"true\">\r\nAccess-Control: allow &lt;$origin&gt;\r\nAccess-Control-Allow-Origin: $origin\r\nAccess-Control-Max-Age: 30\r\nAccess-Control-Allow-Credentials: true\r\nAccess-Control-Allow-Methods: MYMETHOD, STATUS\r\n<\/pre>\n<p>Hierbei gibt $origin die ausgewertete UND gepr\u00fcfte Origin der Seite an. Diesen Wert holt man sich aus dem sowohl im OPTIONS als auch im richtigen Request mitgesendeten Origin-Header, der wie folgt ausschaut:<\/p>\n<pre lang=\"email\">\r\nOrigin: http:\/\/benny-baumann.de\r\n<\/pre>\n<p>Der Response-Header Access-Control-Allow-Methods gibt hierbei eine (vollst\u00e4ndige) Liste aller Request-Methoden an, die neben GET und POST zus\u00e4tzlich erlaubt sein sollen. Nur Methoden, die in dieser Liste auftauchen, werden sp\u00e4ter vom Browser ausgef\u00fchrt. Wichtig ist ferner, dass diese generierten Header sowohl im OPTIONS wie auch in allen ausgef\u00fchrten Methoden mitgesendet werden m\u00fcssen. Vergisst man dies und sendet sie nur im Preflight-Response, wird zwar die eigentliche Anfrage an den eigenen Server ausgef\u00fchrt, der Response an den Browser jedoch verworfen und nicht weiter beachtet.<\/p>\n<p>Nach dem die Details bzgl. der Browser-Implementierung klar sein d\u00fcrften, m\u00fcssen diese Header auch generiert werden. Auch hier kann man die meiste Arbeit mit relativ wenig Aufwand umsetzen:<\/p>\n<pre lang=\"php\" escaped=\"true\">\r\nfunction doHeaders() {\r\n    \/\/Sanitizing the Origin Header ...\r\n    $origin = preg_replace('\/[^\\w-:.\\\/*]\/', '', empty($_SERVER['HTTP_ORIGIN']) ? '*' : $_SERVER['HTTP_ORIGIN']);\r\n\r\n    \/\/Allow access from anywhere ...\r\n    \/\/This will actually always return the origin domain for now, but might get limited later\r\n    header('X-GeSHi-API: v0.1');\r\n    header('Access-Control: allow &lt;'.$origin.'&gt;');\r\n    header('Access-Control-Allow-Origin: ' . $origin);\r\n    header('Access-Control-Max-Age: 30');\r\n    header('Access-Control-Allow-Credentials: true');\r\n    header('Access-Control-Allow-Methods: MYMETHOD, STATUS');\r\n\r\n    \/\/Limit allowed HTTP headers for requests; unused for now.\r\n    \/\/header('Access-Control-Allow-Headers: Accept-Encoding');\r\n}\r\n\r\n\/\/Method names are case-insensitive as per RFC 2616 ...\r\n$method = strtolower($_SERVER['REQUEST_METHOD']);\r\n\r\nswitch ($method) {\r\n    case 'options':\r\n        \/\/ Perform validation of preflight requests\r\n        doHeaders();\r\n        exit; \/\/No output for OPTION requests\r\n    case 'get': \/\/Fall Thru to POST\r\n    case 'post':\r\n        \/\/ Just send the headers and behave like normal ...\r\n        doHeaders();\r\n        break;\r\n    case 'mymethod':\r\n        doHeaders();\r\n        \/\/More headers for this method ...\r\n        break;\r\n    default:\r\n        \/\/Throw an error and send the list of accepted methods\r\n        header('HTTP\/1.1 405 Method Not Allowed');\r\n        doHeaders();\r\n        exit;\r\n}\r\n\r\n\/\/Final processing ...\r\n<\/pre>\n<p>Und das war&#8217;s eigentlich auch schon &#8230; Bliebe nur noch ein kleines Beispiel f\u00fcr die Kommunikation bei einer Anfrage zu geben, um einen kurzen Eindruck f\u00fcr den Preflight-Request zu geben:<\/p>\n<pre lang=\"email\" escaped=\"true\">\r\nPreflight Request: http:\/\/api.geshi.org\/?HelloWorld=23&foo=bar\r\n\r\nOPTIONS \/?HelloWorld=23&foo=bar HTTP\/1.1\r\nHost: api.geshi.org\r\nUser-Agent: Mozilla\/5.0 (Windows; U; Windows NT 5.0; de; rv:1.9.1.7) Gecko\/20091221 Firefox\/3.5.7\r\nAccept: text\/html,application\/xhtml+xml,application\/xml;q=0.9,*\/*;q=0.8\r\nAccept-Language: de-de,de;q=0.8,en-us;q=0.5,en;q=0.3\r\nAccept-Encoding: gzip,deflate\r\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\nKeep-Alive: 300\r\nConnection: keep-alive\r\nOrigin: http:\/\/benny-baumann.de\r\nAccess-Control-Request-Method: MYMETHOD\r\n\r\nHTTP\/1.x 200 OK\r\nDate: Thu, 21 Jan 2010 03:27:45 GMT\r\nServer: Apache\/2.2.14 (Debian)\r\nX-GeSHi-API: v0.1\r\nAccess-Control: allow &lt;http:\/\/benny-baumann.de&gt;\r\nAccess-Control-Allow-Origin: http:\/\/benny-baumann.de\r\nAccess-Control-Max-Age: 30\r\nAccess-Control-Allow-Credentials: true\r\nAccess-Control-Allow-Methods: MYMETHOD, LIST, LISTVERSIONS, LISTLANGUAGES, LISTTHEMES, HIGHLIGHT, CSS, STATUS\r\nVary: Accept-Encoding\r\nContent-Encoding: gzip\r\nContent-Length: 20\r\nKeep-Alive: timeout=15, max=100\r\nConnection: Keep-Alive\r\nContent-Type: text\/html\r\n\r\nActual Request: http:\/\/api.geshi.org\/?HelloWorld=23&foo=bar\r\n\r\nMYMETHOD \/?HelloWorld=23&foo=bar HTTP\/1.1\r\nHost: api.geshi.org\r\nUser-Agent: Mozilla\/5.0 (Windows; U; Windows NT 5.0; de; rv:1.9.1.7) Gecko\/20091221 Firefox\/3.5.7\r\nAccept: text\/html,application\/xhtml+xml,application\/xml;q=0.9,*\/*;q=0.8\r\nAccept-Language: de-de,de;q=0.8,en-us;q=0.5,en;q=0.3\r\nAccept-Encoding: gzip,deflate\r\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\nKeep-Alive: 300\r\nConnection: keep-alive\r\nContent-Type: application\/x-javascript; charset=UTF-8;\r\nReferer: http:\/\/benny-baumann.de\/index.php\r\nContent-Length: 27\r\nOrigin: http:\/\/benny-baumann.de\r\nFirstName=John&LastName=Doe\r\n\r\nHTTP\/1.x 200 OK\r\nDate: Thu, 21 Jan 2010 03:27:46 GMT\r\nServer: Apache\/2.2.14 (Debian)\r\nX-GeSHi-API: v0.1\r\nAccess-Control: allow &lt;http:\/\/benny-baumann.de&gt;\r\nAccess-Control-Allow-Origin: http:\/\/benny-baumann.de\r\nAccess-Control-Max-Age: 30\r\nAccess-Control-Allow-Credentials: true\r\nAccess-Control-Allow-Methods: MYMETHOD, LIST, LISTVERSIONS, LISTLANGUAGES, LISTTHEMES, HIGHLIGHT, CSS, STATUS\r\nVary: Accept-Encoding\r\nContent-Encoding: gzip\r\nContent-Length: 1155\r\nKeep-Alive: timeout=15, max=99\r\nConnection: Keep-Alive\r\nContent-Type: text\/html\r\n<\/pre>\n<p>Bliebe nur noch viel Spa\u00df beim Spielen zu w\u00fcnschen. <\/p>\n<p>P.S.: Auf den Preflight-Request nicht verlassen, da der gecachet wird. Steht aber auch in den W3C-Dokumenten noch mal genau drin \ud83d\ude09<\/p>\n<p class=\"wp-flattr-button\"><a href=\"https:\/\/blog.benny-baumann.de\/?flattrss_redirect&amp;id=491&amp;md5=306cb39d6b9a3bd4f4835c074f30a6a9\" title=\"Flattr\" target=\"_blank\"><img src=\"http:\/\/blog.benny-baumann.de\/wp-content\/plugins\/flattr\/img\/flattr-badge-large.png\" srcset=\"http:\/\/blog.benny-baumann.de\/wp-content\/plugins\/flattr\/img\/flattr-badge-large.png\" alt=\"Flattr this!\"\/><\/a><\/p>","protected":false},"excerpt":{"rendered":"<p>Kleine Vorwarnung vorweg: Es wird technisch! Wem die \u00dcberschrift nichts sagt, empfehle ich vor dem Fortsetzen in diesem Post das Studium diverser Standards. Zudem sei bereits hier angemerkt: Die hier vorgestellte Technik funktioniert auf Grund der Same Origin Policy nicht mit allen Browsern. Getestet (und als Funktionierend bekannt) ist aber mindestens alles, was FF 3.5.X [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0,"footnotes":""},"categories":[29],"tags":[156,253,250,98,15,345,251,21,252,346,249],"class_list":["post-491","post","type-post","status-publish","format-standard","hentry","category-software","tag-apache","tag-api","tag-cors","tag-developement","tag-firefox","tag-geshi","tag-http","tag-php","tag-rest","tag-server","tag-xhr"],"_links":{"self":[{"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/posts\/491","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=491"}],"version-history":[{"count":3,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/posts\/491\/revisions"}],"predecessor-version":[{"id":494,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/posts\/491\/revisions\/494"}],"wp:attachment":[{"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=491"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=491"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=491"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}