PHP und Apache Rewrite von Web-Requests – Ausschluss von Dateien des Typs CSS, JPG, etc. ?

Gestern bin ich in eine klassische Falle im Zusammenhang mit Apache Rewrites gestolpert.

Für ein CMS-Projekt hatte ich in einer ".htacces"-Datei eines Apache-Servers Rewrite-Direktiven für externe HTTP-Requests nach HTML-Dateien hinterlegt. Das CMS arbeitet intern ausschließlich mit PHP-Dateien und Parametern zur Erzeugung von Webseiten. Nach außen hin werden aber reguläre Adressen von HTML-Dateien angeboten. Angeforderte HTML-Seiten müssen daher auf dem Server auf bestimmte Generatorprogramme und zugehörige GET/POST-Parameter abgebildet werden.

Rewriting ist für solche Anforderungen eine Standardlösung (siehe etwa auch das Vorgehen von WordPress):

Der Request wird an eine zentrale PHP-Datei weitergereicht. Diese zerlegt den URL-String der angeforderten HTML-Datei; über Datenbank-Informationen werden dann Parameter für Webseitengeneratoren (PHP-Programme) ermittelt. Die zentrale Datei gibt danach die Kontrolle an die Generatoren ab. Die notwendige Datenbankinformation wird vom CMS bereits während der Anlage und Konfiguration der Webseiten durch den User erzeugt.

Im meinem Fall war ich bzgl. der Rewrite-Anweisung allerdings ein wenig bequem:

Alle (!) Abfragen zu nicht existierenden Dateien wurden zur Behandlung an eine zentrale PHP-Datei "pager.php5" meines CMS verwiesen.

Das funktionierte auch wunderbar - solange nur HTML-Dateien abgefragt wurden, zu denen die Website Links anbot und die im CMS auch mal angelegt worden waren. Traten bzgl. solcher Anfragen Fehler auf oder lies sich aus der Datenbank keine adäquate Info zur angeforderten HTML-Seite ermitteln, wich das PHP-Programm "pager.php5" kontrolliert auf Fehlerroutinen aus.

Nun sah ich bei der Überprüfung des Netzwerkverkehrs bei bestimmten Seiten allerdings, dass es gleich zig-fach zu einem wiederholten Abrufversuch für eine Datei "err_page.php5" in einem bestimmten Bild-Verzeichnis kam; diese PHP-Fehler-Datei existierte dort jedoch gar nicht und war dort auch nie vorgesehen.

Ursachenanalyse

Tatsächlich rufe ich solche PHP-Files zur Behandlung bestimmter Fehler auf, die im CMS im Zuge der Seitengenerierung entstehen können. Allerdings nicht in einem Bildverzeichnis ....

Nach einer Weile fand ich heraus, dass das Problem dennoch durch eine angeforderte, aber auf dem Test-Server nicht vorhandene Bilddatei ausgelöst wurde.

Das war keineswegs so einfach zu erkennen, wie man vielleicht meinen möchte - bei nicht vorhandener Datei übernimmt ja ordnungsgemäß "pager.php5" die Kontrolle - und somit erscheint im Browser nicht zwingend eine Warnung. Eine Warnung auf HTTP-Ebene würde im Einzelfall ja das gezielte Absetzen einer HTTP-Protokoll-Meldung im Verlauf der Situationsbehandlung erfordern. So schlau war ich bei der Konzeption aber nicht gewesen.

Ich dachte deshalb zunächst an einen Fehler in einer PHP-Routine zur automatischen Bildskalierung auf vom CMS-User vorgegebene Größen. Ein Fehler bzw. eine Fehlerbehandlung für nicht existierende Bilddateien in der festgestellten Form lag dort aber nicht vor.

Weitere Tests und ein genauerer Blick in den HTTP-Verkehr zeigten schließlich, dass der "Referrer" der fehlerhaften Datei-Anforderung eine CSS-Datei war! Selbige CSS-Datei existierte und wurde auch ordnungsgemäß gefunden.

Was war das eigentliche Problem?

In der CSS-Datei gab es eine Anweisung der Art

background-image:url(Pfad_zum_(fehlenden)_Bild);

für ein Hintergrundsbild - leider für eines, das auf dem Server nicht existierte.

Der entsprechende Abruf führte dann in Kombination mit der Rewrite-Anweisung zu einer Reaktion nach dem Muster

  • Abruf nicht existierende Datei aus CSS-Anweisung
  • => pager.php5
  • => Auslösen einer "Fehlerbehandlung" durch eine err_page.php5, die aus Gründen mangelnder Voraussicht im Bildverzeichnis erwartet wurde, dort aber nicht existierte
  • => Abruf einer nicht existierenden PHP-Datei
  • => pager.php5 =>. Erneuter Verweis auf Fehlerbehandlung durch eine nicht existierende "err_page.php5"
  • => Abruf einer nicht existierenden PHP-Datei
  • etc., etc.

Apache versucht es dann mehrfach und bricht schließlich ab.

Lösungsansatz 1: Klammere Dateien bestimmter Typen (jpg, png, css, js, ...) aus der Rewrite-Anweisung aus

Das Erlebnis brachte mich dazu, genauer darüber nachzudenken, wie ich eigentlich mit Rewrites normaler Dateien der Typen ".jpg, .gif, .png, .swf, .css, .js" etc. umgehen sollte, für die eine Ersetzung durch PHP-Programme gar nicht vorgesehen ist.

Eine Lösungsvariante ist das Ausklammern dieser Dateitypen von der Rewrite-Anweisung in der ".htaccess"-Datei. Das sieht im einfachsten Fall etwa so aus:

Options +FollowSymLinks
RewriteEngine On
RewriteBase /
RewriteRule ^php/hmenu/pager.php5(.*)$ - [L] 

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule \.(js|css|ico|gif|jpg|png|swf|ttf|eot)$ - [NC,L]

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ my_Rw_Php_Path/pager.php5?adr=$1 [PT]   

 
Hier werden zwei "Condition/Rewrite"-Sequenzen eingesetzt, da ohne besondere Tricks (Skip-Direktiven) zu einem Block aus Condition-Anweisungen nur genau eine Rewrite-Anweisung gehören sollte. "NC" sorgt für eine Nichtbeachtung von Groß-/Klein-Schreibung. "L" beendet die Rewrite-Analyse. "my_Rw_Php_Path" steht für einen Pfad zu einem Serververzeichnis, das die zentralen Programme zur Rewrite-Behandlung beherbergt.

Wird nun eine nicht vorhandene Datei der genannten Typen von einem Web-Client angefordert, wird diese Anforderung durchgereicht und vom Apache-Server mit HTTP-Fehlern der Art "404 Not Found" quittiert. Das reicht in Testphasen zur Prüfung der Lauffähigkeit einer CMS-basierten Website normalerweise aus.

Lösung 2: Behandle fehlende Dateien bestimmter, ausgewählter Typen als Sonderfälle in einer speziellen zentralen PHP-Datei

Eine kontrollierte Reaktion des Systems auf nicht vorhandene Dateien bestimmter Typen jenseits von HTML-Dateien lässt sich natürlich auch in einer weiteren zentralen PHP-Datei (etwa "missing.php5") vorsehen, auf die eine gesonderte Rewrite-Anweisung verweist. Beispielsweise könnte man den mittleren Teil der obigen ".htaccess" in diesem Sinne ersetzen durch:

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)\.(js|css|ico|gif|jpg|png|swf|ttf|eot)$ my_Rw_Php_Path/missing.php5?missadr=$1\.$2 [NC,L]

Bzgl. der Problembehandlung in der "missing.php5" muss man sich aber genau überlegen, für welche Dateien man tatsächlich eine offene und für den User auch erkennbare Fehlermeldung vorsehen will. Ein fehlendes Bild z.B. ist meist nicht überlebenskritisch.

Ich tendiere im Moment dazu, gezielt Meldungen in eine eigene Log-Datei auf dem Server zu schreiben, die man sowohl im Test- als auch Produktivbetrieb regelmäßig auswertet. Ein Minimal-PHP-Skript "missing.php5" könnte für diesen Zweck dann in etwa so aussehen:

<?php
$missadr = 'unknown'; 
if (isset($_GET['missadr']) ) {
	$missadr = $_GET['missadr']; 
}

$fh = fopen("missing.log", 'a+'); 
$out_str = "\r\n" . date('d.m.Y :: H.I.s') . " :: A requested file (" .$missadr . ") is missing"; 
fputs($fh, $out_str); 
fclose($fh); 

header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
exit;
?> 

 
Natürlich wäre das in dieser Einfachheit fahrlässig; der Inhalt von $_GET['missadr'] ist im produktiven Einsatz zu prüfen und ggf. zu bereinigen, um den Inhalt als Teil eines Angriffsvektors auszuschalten. In diesem Artikel geht es aber nur um einen ersten Ansatz.

Der Header-Output ist wichtig; durch ihn kann man z.B. auch in Browser-Tools (bei FF etwa in der Web-Konsole) erkennen, dass ein Fehler vorliegt und eine Datei tatsächlich nicht vorhanden ist.

Ein typischer Output in der Datei "missing.log" hat nach zwei Aufrufen bestimmter Webseite, für die indirekt eine Bilddatei "hg_dxm_7.jpg" angefordert wird, dann ggf. folgenden Inhalt:

27.06.2017 :: 12:0:38 :: A requested file (image/hg_dxm_7.jpg) is missing
27.06.2017 :: 12:0:39 :: A requested file (image/hg_dxm_7.jpg) is missing
27.06.2017 :: 12:0:02 :: A requested file (image/hg_dxm_7.jpg) is missing
27.06.2017 :: 12:0:02 :: A requested file (image/hg_dxm_7.jpg) is missing

Man erkennt hier an der Zeitangabe, dass die fehlende Datei pro Seitenaufruf gleich zweimal angefordert wird; in meinem Fall aus einer CSS-Datei heraus, aber auch direkt über ein HTML-Tag.

Fazit

Nicht nur in einem CMS will man ggf. Requests nach HTML-Dateien durch den gezielten Einsatz von PHP-Webgeneratoren beantworten. Die Nutzer (und auch Suchmaschinen) glauben, reguläre HTML-Dateien abzurufen. In Wirklichkeit sind die Dateien nicht vorhanden; Apache Rewrites sorgen vielmehr für die Erzeugung von HTML-Seiten durch PHP-Programme.

Zu einfach gehaltene Rewrite-Anweisungen für nicht vorhandene Dateien können dabei allerdings schnell zu schwer zu durchschauenden bis rekursiven Fehlern führen. Fordern HTTP-Requests evtl. nicht vorhandene Dateien eines bestimmten Typs an, für die eine gezielte Ersetzung gar nicht vorgesehen ist, so hängt es allein von der Voraussicht der Entwickler ab, was im Detail über Ersetzungen passiert. Es empfiehlt sich deshalb, solche Datei-Anforderungen

  • entweder von vornherein aus der Rewrite-Behandlung auszuschließen
  • oder sie aber einer gezielten Sonderbehandlung durch eine eigene PHP-Datei zuzuführen. Dabei sollten angemessene HTTP-Antwortcodes erzeugt werden.

Apache Rewrite für zwei Domänen, die gemeinsame Datei-Ressourcen nutzen

Gestern wurden wir mit zwei Websites konfrontiert, die wir um einen Blog ergänzen sollten. Die Webserver-Installation, die wir vorfanden, war interessant und hat uns ein wenig beschäftigt:

Zwei Domain-Namen (alpha.de und beta.de) waren beim Hosting-Provider mit ein und demselben Webserver-Verzeichnis verbunden. Dieses wiederum wies zwei Unterverzeichnisse auf, in denen sich vor allem HTML-Seiten zu den verschiedenen Domänen befanden. Die Idee hinter diesem Setup war wohl, von den Webseiten beider Domänen aus gemeinsame Datei-Ressourcen im Hauptverzeichnis zu nutzen.

Setup zur Nutzung gemeinsamer Datei-Ressourcen durch zwei Web-Domänen auf demselben Webserver

Nennen wir das Hauptverzeichnis mal dir_domains und die Subverzeichnisse dir_alpha und dir_beta.

Die Webseiten von alpha.de und beta.de nutzen gleiche Datei-Ressourcen (CSS-Dateien, Javascripts, PHP-Programme, ...). Entsprechende Verzeichnisse fanden sich unter dir_domains:

dir_domains (Webserververzeichnis für Domains alpha/beta)
|__css
|__js
|__php
|__index.php
| ....
|
|__dir_alpha
|        |__index.html
|        |...(HTML-Dateien für die Domäne beta.de)
|        | ....
|__dir_beta
         |__index.html
         |... (HTML-Dateien für die Domäne beta.de)
         |...

Die Datei index.php übernahm die Zugangssteuerung für die Startseiten: Je nach Domain-Anforderung (alpha.de oder beta.de) des Users wurde dieser auf die jeweilige Index-Seite unter dir_alpha oder dir_beta umgelenkt. So weit, so gut - oder eher so schlecht ....

Nach Inaugenscheinnahme der beiden Domänen im Internet war mir klar, warum das so aufgesetzt worden war:

Die Webseiten unter alpha.de bzw. beta.de haben einen ganz ähnlichen Aufbau und nutzen gleiche PHP-Programme und Scripts. Da bei der Anforderung von Ressourcen wie CSS-/JS-/PHP-Dateien Domaingrenzen in der Verzeichnisstruktur des Web-Servers normalerweise nicht überschritten werden dürfen, waren die Web-Designer gezwungen gewesen, beiden Domänen dasselbe Hauptverzeichnis des gehosteten Webserver-Accounts zuzuweisen. Zur Kompensation musste eine Art Umlenkung auf die jeweiligen Verzeichnisstrukturen integriert werden. (Anmerkung am Rande: Die Domängrenzen, im Sinne von Verzeichnisgrenzen, gelten übrigens nicht für Include-Dateien, die in PHP-Programmen nachgeladen werden. Solche Include-Dateien kann man ruhig oberhalb des Domainverzeichnisses unterbringen!).

Was war am Setup schlecht?

Während die Nutzung gemeinsamer Ressourcen durchaus zu befürworten ist, war die "Umlenkung" durch das PHP-Skript schlecht gelöst. Sie funktionierte eigentlich nur für den Aufruf einer der beiden Domän-Namen selbst (Umlenkung auf die jeweilige index.html-Seiten). Die Navigation innerhalb der Webseiten einer Domäne war über relative Pfade gelöst; sobald ein User sich einmal innerhalb einer Domäne bewegte, funktionierte deshalb alles aus Nutzersicht alles bestens.

Aber: Der Aufruf einer spezifischen Seite einer Domäne über eine direkte Eingabe in die Adresszeile des Browsers - z.B. http://alpha.de/infos/impressum.html - funktionierte mit dem vorhandenen PHP-Script nicht. Das Script index.php kümmerte sich nur um die Index-Seiten der Domänen. Es wurde als Index-Datei ja nur dann aktiv, wenn eine der Domän-Adressen alpha.de oder beta.de ohne weitere Zusätze im Browser aufgerufen wurde.

Lösungsansatz über Apache Rewrite

Bei dem gehosteten Webserver handelte es sich um einen Apache-Server. Eine saubere Lösung für den gewünschten Setup-Ansatz mit geteilten Datei-Ressourcen besteht dann natürlich darin, das Apache Rewrite-Modul (mod_rewrite) zu nutzen. Die Servereinstellungen beim Provider waren so, dass die Rewrite Engine über lokale ".htaccess"-Dateien aktiviert und gesteuert werden konnte. Wir haben dann folgenden einfachen Vorschlag umgesetzt:

Inhalt der .htaccess-Datei für das Verzeichnis dir_domains:

Options +FollowSymLinks 
RewriteEngine On 
RewriteBase /

RewriteRule ^alpha/(.*)$ - [L]
RewriteRule ^beta/(.*)$ - [L]

RewriteRule ^css/(.*)$ - [L]
RewriteRule ^images/(.*)$ - [L]
RewriteRule ^php/(.*)$ - [L]
RewriteRule ^script/(.*)$ - [L]

RewriteCond %{SERVER_NAME} alpha.de [OR]
RewriteCond %{SERVER_NAME} www.alpha.de
RewriteRule ^$ alpha/index.html [L]

RewriteCond %{SERVER_NAME} alpha.de [OR]
RewriteCond %{SERVER_NAME} www.alpha.de
RewriteRule ^(.*)$ alpha/$1 [NC,L]

RewriteCond %{SERVER_NAME} beta.de [OR]
RewriteCond %{SERVER_NAME} www.beta.de
RewriteRule ^$ beta/index.html [L]

RewriteCond %{SERVER_NAME} beta.de [OR]
RewriteCond %{SERVER_NAME} www.beta.de
RewriteRule ^(.*)$ beta/$1 [NC,L]

 
Die ersten 2 Rewrite-Regeln sorgen dafür, dass keine Umlenkung mehr vorgenommen wird, wenn man sich bereits im Verzeichnisbereich der Seite befindet. Dieser Vorspann ist wichtiger, als man meinen möchte: die nachfolgenden Regeln würden für sich allein zu einer unbegrenzten Iteration von Rewrites führen!

Die nächsten 4 Regeln sorgen dann dafür, dass Verweise auf die gemeinsam genutzten Ressourcen-Verzeichnisse und zugehörige GET-Anforderungen an den Server unangetastet bleiben. Diese Verzeichnisse werden ja aus dem HTML-Code der Webseiten unter dem alpha- bzw. dem beta-Sub-Verzeichnis referenziert; z.B. über relative Pfade.

Danach kommen die eigentlichen Umlenkungsregeln. Wir haben hier die Grundregel befolgt, dass eine oder mehrere Rewrite Conditions sich nur und ausschließlich auf die nächste Rewrite Rule beziehen - also auf genau eine Rewrite Rule! Das wird in der hektik oft übersehen; es gibt keine native Klammerung mehrerer Rewrite Rules zu Rewrite Conditions. Allerdings kann man mit Negationen der Bedingungen und Skip-Zusätzen hinter den Rewrite-Regeln tricksen. Um das Verständnis nicht zu erschweren, haben wir hier auf solche Hacks verzichtet.

Wird keine Dateiname angegeben - ist also der Pfad hinter dem Domain-Anteil leer - wird auf die jeweilige Index-Seite umgelenkt. Sind konkrete Webseiten einer Domäne angefordert, wird auf die gewünschte Datei im jeweiligen Unterverzeichnis verwiesen.

Das war es schon; die Datei index.php kann und sollte danach gelöscht werden. Unser Kunde kann nun 2 Domänen im gleichen Webserververzeichnis nutzen und zwischen beiden Implementierungen gemeinsame Ressourcen teilen. Eigentlich eine nette kleine Geschichte, die ich vielleicht auch mal für eigene Sites nutzen werde - Apache sei Dank!