Download der Whois Server Liste mittels HTTP GET Request

Wenn Sie eine Whois Abfrage für verschiedene Top Level Domains (TLD) programmieren möchten, dann müssen Sie eine Whois Server Liste verwenden. Das liegt einfach daran, dass jede TLD der Verantwortung einer bestimmten Behörde unterliegt, und jeder seinen eigenen Whois Server verwendet. Dabei gibt es auch keine Regel unter welcher Adresse sich der Whois Server befindet. Zwar findet man die meisten unter whois.nic.tld, aber eben nicht alle. Dazu kommt auch noch dass es kein festgelegtes Format für die Ausgabe eines Whois Servers gibt.

In diesem PHP Tutorial wird die Whois Server Liste von domaininformation.de verwendet, welche unter der URI http://serverlist.domaininformation.de/ zu finden ist. Diese Liste ist ziemlich umfangreich und eben auch im maschinenfreundlichen XML Format vorhanden.

Wir werden hier die Klassen Request, HTTP_Request, Header und File implementieren. Wenn Sie sich näher mit dem Thema Klassen in PHP vertraut machen wollen, empfiehlt sich ein Blick ins Kapitel 14. Klassen und Objekte im PHP-Manual.

Die File Klasse

Die File Klasse kapselt Dateioperationen ab.
Als Attribute braucht sie den Dateiname ($file), der im Konstruktor initialisiert wird und die Resource ID für die Fileoperationen ($id).
fopen() öffnet eine Datei. Dabei muss man bereits angeben was für Operationen man ausführen möchte.
fwrite() schreibt in eine geöffnete Datei, fgets() liest aus einer offenen Datei und feof() sagt, wann das Dateiende erreicht ist. Zum Schließen kann man fclose() benutzen, allerdings werden alle Resourcen von PHP am Ende automatisch freigegeben. Mit file_exists() erfährt man ob die Datei bereits existiert. filemtime() sagt wann die letzte Änderung war.
Die File Klasse kriegt nun eigene Methoden, die diese Aufgaben übernehmen.

1   class File {
2   
3   	var $file, $id;
4   
5   	function File($file) {
6   		$this->file = $file;
7   	}
8   
9   	function open($mode) {
10  		$this->id = fopen($this->file, $mode);
11  		if (!$this->id) {
12  			trigger_error('Could not open '.$this->file.' in mode '.$mode.'.');
13  		}
14  		return $this->id;
15  	}
16  
17  	function close() {
18  		fclose($this->id);
19  	}
20  
21  	function read($bytes = 1024) {
22  		return fgets($this->id, $bytes);
23  	}
24  
25  	function write($data) {
26  		fwrite($this->id, $data);
27  	}
28  
29  	function exists() {
30  		return file_exists($this->file);
31  	}
32  
33  	function eof() {
34  		return feof($this->id);
35  	}
36  
37  	function time() {
38  		return ($this->exists() ? filemtime($this->file) : false);
39  	}
40  
41  	...
42  
43  }

Da dadurch noch nichts gewonnen ist, werden nun weitere Methoden hinzugefügt, die etwas mehr können.
write_once() wird die Datei zum Schreiben öffnen, die Daten schreiben und wieder schließen.
read_all() gibt den gesamten Inhalt einer geöffneten Datei aus. Das geschieht in einer While-Schleife die bis zum Ende der Datei (EOF) liest.
read_once() wird die Datei zum Lesen öffnen, mit read_all() alle Daten lesen, die Datei schließen und die Daten zurück geben.
is_uptodate() prüft an Hand eines Timestamps als Argument, ob die Datei aktuell ist.

1   class File {
2   
3   	...
4   
5   	function write_once($data) {
6   		if (!$this->open('w')) {
7   			return false;
8   		}
9   
10  		$this->write($data);
11  		$this->close();
12  		return true;
13  	}
14  
15  	function read_all() {
16  		$buffer = '';
17  		while (!$this->eof()) {
18  			$buffer .= $this->read();
19  		}
20  
21  		return $buffer;
22  	}
23  
24  	function read_once() {
25  		if (!$this->open('r')) {
26  			return false;
27  		}
28  
29  		$buffer = $this->read_all();
30  		$this->close();
31  		return $buffer;
32  	}
33  
34  	function is_uptodate($time) {
35  		return $this->time() >= $time;
36  	}
37  
38  }

Damit hat unsere File Klasse alles was wir im weiteren Verlauf noch benötigen.

Die Request Klasse

Diese Klasse wird später der HTTP_Request und der WHOIS_Request Klasse als Elternklasse dienen. Ihre Funktion ist es die Verbindungsoperationen abzukapseln. D.h. Diese Klasse ist für die gesamte Kommunikation mit dem Server verantwortlich.

Als Attribute benötigt die Request Klasse die Adresse des Servers ($server), eine Resource ID für die Verbindung ($id), den Port ($port) und einen Timeout ($timeout).

1   class Request {
2   
3   	var $server, $port, $timeout,
4   	    $id = false;
5   
6   	...
7   
8   }

Der Konstruktor initialisiert die Attribute $server, $port und $timeout.

1   class Request {
2   
3   	...
4   
5   	function Request($server, $port, $timeout = 15) {
6   		$this->Constructor($server, $port, $timeout);
7   	}
8   
9   	function Constructor($server, $port, $timeout = 15) {
10  		$this->server  = $server;
11  		$this->port    = $port;
12  		$this->timeout = $timeout;
13  	}
14  
15  	...
16  
17  }

Die Funktion fsockopen() stellt eine Verbindung mit dem Server auf dem angegebenen Port her und fclose() schließt diese wieder. Wobei das explizite Schließen einer Verbindung nicht nötig ist, da PHP am Ende alle seine Resourcen wieder frei gibt.
Zum Senden wird die Funktion fwrite() und zum Empfangen fread() (bzw. fgets() zum zeilenweisen Lesen) benutzt.
feof() stellt fest ob eine Verbindung beendet wurde.
Mit diesen PHP Funktionen können wir nun unsere Klasse mit Methoden zum Datentransfer ausstatten.

1   class Request {
2   
3   	...
4   
5   	function connect() {
6   		$this->id = @fsockopen($this->server, $this->port, $dummy, $dummy, $this->timeout);
7   		if (!$this->id) {
8   			trigger_error('Could not connect to host '
9   			              .$this->server.' at port '.$this->port.'.');
10  			return false;
11  		}
12  
13  		return true;
14  	}
15  
16  	function close() {
17  		fclose($this->id);
18  	}
19  
20  	function write($data) {
21  		fwrite($this->id, $data);
22  	}
23  
24  	function read($bytes = 1024) {
25  		return fread($this->id, $bytes);
26  	}
27  
28  	function readln($bytes = 1024) {
29  		return fgets($this->id, $bytes);
30  	}
31  
32  	function eof() {
33  		return feof($this->id);
34  	}
35  
36  	...
37  
38  }

Diese Low Level Methoden sind noch nicht sonderlich spektakulär.
Deswegen erweitern wir die Klasse ein letztes mal mit etwas nützlicheren Methoden.
Die Methode readAll() empfängt die kompletten Daten. Dazu wird einfach in einer While-Schleife bis zum EOF gelesen.
doRequest() wird eine Methode, die sich komplett um die Verbindung, das Senden der Daten und das Empfangen der Antwort kümmert.

1   class Request {
2   
3   	...
4   
5   	function readAll() {
6   		$buffer = '';
7   		while (!$this->eof()) {
8   			$buffer .= $this->read();
9   		}
10  		return $buffer;
11  	}
12  
13  	function doRequest($data_to_send) {
14  		if (!$this->connect()) {
15  			return false;
16  		}
17  
18  		$this->write($data_to_send);
19  		$result = $this->readAll();
20  		$this->close();
21  
22  		return $result;
23  	}
24  
25  }

Die Request Klasse ist nun fertig.
Sie wird uns im Laufe des Tutorials noch ein paar mal als Basisklasse dienen. Denn das schöne an dieser Klasse ist, dass mit der doRequest() Methode alle Verbindungsoperationen abgekapselt sind.

Die Header Klasse

Bevor wir zur HTTP_Request Klasse kommen brauchen wir eine Header Klasse, da wir uns auch um spezielle Header zum Senden und Empfangen kümmern müssen.
Das Hypertext Transfer Protocol (HTTP) verwendet am Anfang eines Request oder einer Response sogenannte Header. Das sind Zeilen, die nach dem Schema "Header-Name: Header Wert" aufgebaut sind. Die einzige Ausnahme ist die Status-Line, und die Request-Line.
Wofür die einzelnen Header gut sind, muss uns hier noch nicht interessieren.
Diese Klasse soll einfach nur aus einem String die Header finden und in einem Array speichern. Und das ganze auch in die andere Richtung, also aus dem Array einen validen String mit HTTP-Headern erzeugen.

Als erstes erstellen wir ein triviales Grundgerüst.
Als Attribute brauchen wir nur ein Array mit den Headern ($header) und ein Attribut für die erste Zeile ($firstLine).
Und dann brauchen wir noch eine Methode addHeader() um das Array zu erweitern, setFirstLine() zum setzen der ersten Zeile, getFirstLine() zum lesen der ersten Zeile, eine exists() um zu prüfen ob ein Header existiert und getHeader() um eben einen Eintrag aus dem Array anzuzeigen.

1   class Header {
2   
3   	var $header = array(),
4   	    $firstLine;
5   
6   	function addHeader($name, $value) {
7   		$this->header[$name] = $value;
8   	}
9   
10  	function setFirstLine($line) {
11  		$this->firstLine = $line;
12  	}
13  
14  	function getFirstLine() {
15  		return $this->firstLine;
16  	}
17  
18  	function exists($key) {
19  		return isset($this->header[$key]);
20  	}
21  
22  	function getHeader($key) {
23  		return $this->header[$key];
24  	}
25  
26  	...
27  
28  }

RegExp - Reguläre Ausdrücke

Wir möchten nun aus einer HTTP-Response String unser Header Objekt basteln. Das wird die Methode parseResponse() machen.
Zum Finden der Header bieten sich Reguläre Ausdrücke an. Reguläre Ausdrücke sind Ausdrücke aus einer Sprache die zum Suchen in Strings gedacht ist. Diese Thematik ist zu umfangreich um hier ins Detail zu gehen.
Zum Finden des Status-Codes wird noch kein Regulärer Ausdruck benötigt. Das ist immer die erste Zeile. Hier reichen die PHP Funktionen strpos() und substr() aus. Man sollte nach Möglichkeit immer auf Reguläre Ausdrücke verzichten, da die Engine, die dahinter steckt, doch einige Resourcen verbraucht.

1   class Header {
2   
3   	...
4   
5   	function parseResponse($response) {
6   		$firstBreak = strpos($response, "\r\n");
7   		if (!$firstBreak) {
8   			return false;
9   		}
10  
11  		$this->setFirstLine(substr($response, 0, $firstBreak));
12  		$response = substr($response, $firstBreak + 2);
13  
14  		...
15  
16  	}
17  
18  	...
19  
20  }

Das Suchmuster /(.+) *?: *?(.+) *?\r\n/U findet alle Header.
Um dieses Muster zu erklären sollte man am besten mit dem Modifier U anfangen. Er steht für ungreedy, was soviel heißt wie ungierig. Normalerweise versucht ein Quantifier (?, *, + und {n,m}) soviel wie möglich mitzunehmen, dass ist aber bei uns nur teilweise erwünscht, weil dann schnell aus mehreren Headern nur ein einziger wird. Also setzen wir das grundsätzliche Verhalten mit dem U Modifier auf ungreedy, und tun es nur an den Stellen wo es erwünscht ist mittels einem ? nach dem Quantifier auf greedy setzen.
Der Punkt steht für jedes beliebige Zeichen, und das + heißt, dass das Zeichen davor mindestens einmal vorkommt, aber auch öfters. Klammern werden zur Referenzierung benötigt. * heißt das das Zeichen davor keinmal oder beliebig oft vorkommen darf.
Also übersetzt heißt dann obiges Muster:

Alle Zeichen bis zum Doppelpunkt, wobei diese Zeichen, ohne Leerzeichen am Ende, referenziert werden können (das wird dann unser Headername). Ab dem ersten Zeichen nach dem Doppelpunkt, welches kein Leerzeichen ist, und dem letzten Zeichen in dieser Zeile, welches auch kein Leerzeichen ist, wird auch nochmal referenziert (das wird dann der Wert des Headers).

PHPs preg_match_all() Funktion wird die gefundenen Strings in einem zweidimensionalen Array speichern, welches wir in einer For-Schleife durchlaufen. Die Header Namen werden dann mittels strtolower() kleingeschrieben in dem $header Array gespeichert.

1   class Header {
2   
3   	...
4   
5   	function parseResponse($response) {
6   
7   		...
8   
9   		if (preg_match_all('/(.+) *?: *?(.+) *?\r\n/U', $response, $matches)) {
10  			$keys	= $matches[1];
11  			$values	= $matches[2];
12  			for ($i = 0; $i < count($keys); $i++) {
13  				$this->addHeader(strtolower($keys[$i]), $values[$i]);
14  			}
15  		}
16  
17  		return true;
18  	}
19  
20  	...
21  
22  }

Jetzt fehlt nur noch eine makeRequest() Methode, welche zu dem Header Objekt ein Request String ausgibt. Naja, dass ist nicht weiter schwierig. Der String kriegt als erstes $firstLine und dann geht's in einer Foreach-Schleife durch das Array und die Header werden angefügt. Dann noch ein abschließendes \r\n, denn ein Request endet mit \r\n\r\n und fertig ist der Request String.
Und um spätere Abfragen Strings in Header zu erleichtern fügen wir noch eine inValue() Methode hinzu, die prüft ob der Header existiert und der String im Header vorkommt.

1   class Header {
2   
3   	...
4   
5   	function makeRequest() {
6   		$request = $this->firstLine."\r\n";
7   		foreach ($this->header as $name => $value) {
8   			$request .= $name.': '.$value."\r\n";
9   		}
10  		return $request."\r\n";
11  	}
12  
13  	function inValue($header, $value) {
14  		return $this->exists($header)
15  		    && strpos(strtolower($this->getHeader($header)), $value) !== false;
16  	}
17  
18  }

Das war's die Header Klasse ist komplett. Die HTTP_Request Klasse wird Header Objekte beim Lesen und Schreiben verwenden.

Die HTTP_Request Klasse

Endlich kommen wir zur HTTP_Request Klasse, die für den Download zuständig ist.
Sie bietet Methoden um die Serverliste nach den Richtlinien runterzuladen an.

HTTP GET Request

Die HTTP_Request erbt die Request Klasse. Da sich der HTTP Request immer auf Port 80 abspielt werden wir den Konstruktor um das $port Argument abspecken. Ausserdem initialisiert der Konstruktor das zusätzliche Attribut $request. Das $request Attribut enthält ein Header Objekt welches für den Request benutzt wird. Es werden dann gleich alle benötigten Header gesetzt:

  • Host Header

    Er beinhaltet den Namen des Servers.
    Der Host Header existiert seit HTTP 1.1, wurde allerdings schon vorher eingesetzt. Dieser Header wurde mit der Einführung von virtuellen Hosts notwendig. Wenn mehrere Domainadressen zu einer IP-adresse zeigen, aber jede Domainadresse unterschiedlichen Content liefern soll, dann spricht man von einem virtuellen Host.
    Der Host-Header liefert dabei die einzige Möglichkeit um dem Server zu sagen an welchen virtuellen Host der Request geht.
  • Accept-Encoding Header

    Mit dem Accept-Encoding Header wird dem Server gesagt, wie der Content kodiert wird. Also in unserem Fall möchten wir einen GZIP komprimierten Inhalt, um Traffic (und damit auch Zeit) zu sparen.
  • Connection Header

    Seit HTTP 1.1 gibt es auch persistente Verbindungen. D.h. man kann mit einer Verbindung mehrere Requests absetzen.
    Wenn kein Connection Header gesendet wird, übernimmt der Server die Verantwortung für den Verbindungsabbruch. Und in der Regel wird das nicht nach der ersten Verbindung geschehen. D. h. der Server wartet ob er noch einen Request erhält.
    Connection: close hingegen sorgt dafür, dass die Verbindung nach diesem einen Request beendet wird.
1   class HTTP_Request extends Request {
2   
3   	var $header;
4   
5   	function HTTP_Request($server, $timeout = 15) {
6   		$this->Constructor($server, $timeout);
7   	}
8   
9   	function Constructor($server, $timeout = 15) {
10  		parent::Constructor($server, 80, $timeout);
11  
12  		$this->header = &new Header();
13  		$this->header->addHeader('Host', $server);
14  		$this->header->addHeader('Accept-Encoding', 'gzip');
15  		$this->header->addHeader('Connection', 'close');
16  	}
17  
18  	...
19  
20  }

Die write() Methode wird auch überschrieben. Die neue write() erhält als Attribut einen String der den Pfad zur Serverliste auf dem Server enthält (also in unserem Fall "/"). Aus diesem String wird nun die Request-Line im Header Objekt gesetzt. Nun kann die write() mit dem Header Objekt einen Request absetzen.

1   class HTTP_Request extends Request {
2   
3   	...
4   
5   	function write($file) {
6   		$this->header->setFirstLine('GET '.$file.' HTTP/1.1');
7   		parent::write($this->header->makeRequest());
8   	}
9   
10  	...
11  
12  }

Nachdem wir nun den Request absetzen können müssen wir die Response verarbeiten.
Die Response gliedert sich in Header und Content. Wir brauchen nun eine readHeader(), welche den Header liest und ein Header Objekt zurück gibt und eine readContent(), die den Inhalt zurückgibt.

Die readHeader() ist noch relativ trivial. Es wird einfach bis zum ersten auftreten von \r\n\r\n gelesen und den Rest übernimmt parseHeader() von unserem Header Objekt.

readContent() hingegen muss richtige Arbeit verrichten. Zum einen haben wir einen GZIP kodierten Content angefordert, welcher aber von PHP mit gzinflate() dekodiert wird. Dennoch wird es etwas komplexer weil wir im HTTP 1.1 einen zerhackten Content geliefert kriegen. D.h. der Inhalt wird in Chunks gegliedert.
Um die Chunks wieder zusammenzufügen nehmen wir uns zwei Hilfsmethoden. nextChunkSize() gibt die Größe des nächsten Chunks aus und readChunk() liest den nächsten Chunk.

nextChunkSize() liest einfach die nächste Zeile. Wir entfernen die letzten zwei Zeichen dieser Zeile, welche \r\n sind und jagen diese Hexadezimalzahl durch hexdec().

readChunk() gibt false zurück, wenn nextChunkSize() den last-chunk (0) zurück gibt. Ansonsten wird eben soviele Bytes wie nextChunkSize zurück gegeben hat gelesen, plus zwei weitere, die das obligatorische \r\n nach dem Chunk sind.

Jetzt kann readContent() in einer While-Schleife solange lesen, bis readChunk() false zurück gibt. Der Content wird um seine ersten 10 Zeichen erleichtert (der GZIP Header) und gzinflate() gibt die entpackte Serverliste aus.

1   class HTTP_Request extends Request {
2   
3   	...
4   
5   	function &readHeader() {
6   		$header = '';
7   		do {
8   			$header .= $this->readln();
9   		} while (!$this->eof() && substr($header, -4) != "\r\n\r\n");
10  
11  		if (substr($header, -4) != "\r\n\r\n") {
12  			trigger_error('no valid HTTP Response: '.$header);
13  			return false;
14  		}
15  
16  		$headerObj = &new Header();
17  		$headerObj->parseResponse($header);
18  
19  		return $headerObj;
20  	}
21  
22  	function readChunk() {
23  		$size = $this->nextChunkSize();
24  		if (!$size || $size < 1) {
25  			return false;
26  		}
27  		$chunk = $this->read($size);
28  		$this->read(2);
29  		return $chunk;
30  	}
31  
32  	function nextChunkSize() {
33  		$size = $this->readln();
34  		return hexdec(substr($size, 0, -2));
35  	}
36  
37  	function readContent(&$header) {
38  		if ($header->inValue('transfer-encoding', 'chunked')) {
39  			$content = '';
40  			while ($chunk = $this->readChunk()) {
41  				$content .= $chunk;
42  			}
43  		} else {
44  			$content = $this->readAll();
45  		}
46  
47  		return ($header->inValue('content-encoding', 'gzip') ?
48  		        gzinflate(substr($content, 10)) : $content);
49  	}
50  
51  	...
52  
53  }

Zu guter letzt kriegt die HTTP_Request Klasse eine download() Methode. Diese Methode kümmert sich um alles. Sie verbindet sich mit dem Server, sendet den Request, prüft ob die Datei aktuell ist und schreibt gegebenenfalls die neue Datei.

1   class HTTP_Request extends Request {
2   
3   	...
4   
5   	function download($path, &$file) {
6   		if (!$this->connect()) {
7   			return false;
8   		}
9   
10  		$this->write($path);
11  		$header = &$this->readHeader();
12  
13  		if (!$header) {
14  			$this->close();
15  			return false;
16  		}
17  
18  		if ($header->exists('last-modified')
19  		&& $file->is_uptodate(strtotime($header->getHeader('last-modified')))) {
20  			return true;
21  		}
22  
23  		$content = $this->readContent($header);
24  		$this->close();
25  		if (!$content) {
26  			return false;
27  		}
28  
29  		return $file->write_once($content);
30  	}
31  
32  }

Mit diesen Klassen ist es uns möglich die Whois Server Liste runterzuladen. Dabei werden auch die Bedingungen, den Content zu komprimieren und nur eine aktuelle Serverliste runterzuladen, erfüllt.
Ein Anwendungsbeispiel könnte dann so aussehen:

1   $request = &new HTTP_Request('serverlist.domaininformation.de');
2   $file = &new File('serverlist.xml');
3   
4   if ($request->download('/', $file)) {
5   	echo 'Success';
6   } else {
7   	die('Could not save the serverlist.');
8   }

PHP Manual Referenz