Fehler in Http-Klasse

B

Bgag

Hallo!
Im Rahmen eines kleines Projekts bin ich dabei eine HTTP-Klasse zu basteln. Leider ist es mir nicht möglich eine der bestehenden PHPlibs zu verwenden, da das ganze möglichst ohne AddOns funktionieren soll, um die Anforderungen an den Server niedrig zu halten. Das mit der Klasse klappte bisher ganz gut, aber es fehlen noch einige Dinge, die aber noch hinzugefügt werden sollen.

Laufplan
v0.3 Hinzufügen von GET
v0.4 Hinzufügen von POST
v0.5 Hinzufügen weiterer Verbindungsarten (proxy / ssl)
v0.6 bis v0.9 Fehlerbehebung
v1.0 Final

Es bleibt also noch etwas zu tun. Leider ist in der Klasse ein Fehler, den ich nicht finde. Um genau zu sein liegt der Fehler irgendwo in der divideReply()-Methode. Diese soll die Antwort des Servers auf einen HTTP-Request zerlegen und in ein Array speichern.

In einem Testaufruf, übermittel ich via get() Parameter an eine Datei test.php, die entweder "Hello World" oder "Try again" (abhängig vom übermittelten Parameter) zurückgeben soll. Das funktioniert auch soweit, doch wird noch irgendwelcher Mist vor und hinter dieser Ausgabe mit zurückgegeben. Zudem wird der Content-Type als einziger Parameter nicht in das Array $data aufgenommen, obwohl er ebenfalls zurückgeliefert wird. Wo liegt also mein Fehler? Die Dateien test.php und HttpConnect.php, die auch den Testaufruf enthält habe ich unten angehängt. Bin für jede Hilfe sehr dankbar, da mir bisher in anderen Foren noch nicht geholfen werden konnte.
MfG, Andy

test.php
PHP:
<?php
	if( $_GET['num'] == 45)
	{
		echo "Hello world!";
	}
	
	else
	{
		echo "Try again!";
	}
	
?>

HttpConnect.php
PHP:
<?php  
error_reporting(E_ALL);

/***
* HttpConnect class allows easy connecting to a foreign server.
*  
* @package HttpConnect
* @version 0.2
* @author Andreas Wilhelm <Andreas2209@web.de>
* @copyright Andreas Wilhelm
**/  
class HttpConnect
{
	// protected class variables
	protected $host;
	protected $port; 
	
	protected $logStats;
	protected $log;

	/**
	* Constructor - Is called when the class is instanced
	*
	* @access: public
	* @param Str $host
	* @param Int $port
	* @param Bool $log	
	* @return NONE
	*/
	public function __construct($host='localhost', $port=80, $log = false)
	{
		// set server-variables
		$this->host = $host;
		$this->port = $port;
		
		// set if the connection with the server should be logged
		$this->logStats = $log;
	}

	/**
	* login() - Connects to the server
	*
	* @access: private
	* @param Str $host
	* @param Int $port
	* @return Handle
	*/
	private function login()
	{		
		// control-connection handle is saved to $handle
		$handle = @fsockopen($this->host, $this->port);
		if (!$handle)
			throw new Exception("Connection failed.\n"); 
		
		// switch to non-blocking mode - just return data no response
		set_socket_blocking($handle, true);
		
		// set timeout of the server connection
		stream_set_timeout($handle, 0, 200000);
		
		return $handle;
	}
  
	/**
	* logout() - Closes the connection
	*
	* @access: private
	* @return NONE
	*/      
	private function logout($handle)
	{
		fclose($handle);
	}

	/**
	* sendCmd() - Sends a command to the server
	*
	* @access: public
	* @param Str $cmd
	* @return NONE
	*/
	public function sendCmd($handle, $cmd)
	{
		// send the request
		fputs($handle, $cmd);
		
		// log the request
		$this->log("-> $cmd");
	}
	
	/**
	* readSock() - Reads out the response from the server
	*
	* @access: private
	* @return String
	*/
	private function readSock($handle)
	{	
		$response = "";

		while(!feof($handle))
		{
			$response .= fread($handle, 1025);
		}
		
		$this->log($response);
		
		return $response;
	}
			
	/**
	* divideReply() - Spilts up the reply into the different information
	*
	* @access: private
	* @param Str $reply
	* @return NONE
	*/
	private function divideReply($reply)
	{
		// separate header and content
		list($head, $content) = split("\r\n\r\n", $reply);
		
		// unpack header information
		$header = array();

		if( preg_match_all('#(.*): (.*)\r\n#U', $head, $result) )
		{
			$num = count($result[0]);

			for($i=0; $i<$num; $i++)
			{
				$name = $result[1][$i];
				$info   = $result[2][$i];

				$header[$name] = $info;
			}
		}
		
		$data = array('head'=>$header, 'content'=>$content);
		
		return $data;
	}
			
	/**
	* head() - Returns the HTTP-header like a GET or POST reply but without the file
	*
	* @access: public
	* @param Str $uri
	* @param Str $params
	* @param Str $cookies
	* @return NONE
	*/
	public function head($uri='/', $params=false, $cookies=false)
	{
		// login to the server
		$handle = $this->login();
		
		// prepare uri for request
		if (empty($uri) || ($uri{0}!='/'))
		{
			$uri = "/$uri";
		}
				
		// check if parameters should be used and prepare them for request
		if (($params!=false) && (!empty($params)))
		{
			$params = "?$params";
		}
		
		else
		{
			$params = '';
		}
		
		// check if cookies should be used and prepare them for request
		if (($cookies!=false) && (!empty($cookies)))
		{
				$cookies = "Cookie: $cookies\r\n";
		}
		
		else
		{
			$cookies = '';
		}
		
		// prepare host-data
		$host = $this->host;
		if ($this->port != 80)
		{
			$host .= ':'.$this->port;
		}
		
		// send request to the server
		$this->sendCmd($handle, "HEAD $uri"."$params HTTP/1.1\r\n");
		$this->sendCmd($handle, "Host: $host\r\n$cookies");
		$this->sendCmd($handle, "Connection: close\r\n\r\n");
		
		// get server response
		$reply = $this->readSock($handle);
		
		// end connection to the server
		$this->logout($handle);
		
		// prepare headers and file
		$data = $this->divideReply($reply);
		
		return $data;
	}
			
	/**
	* get() - Returns the HTTP-header and the output of the file
	*
	* @access: public
	* @param Str $uri
	* @param Str $params
	* @param Str $cookies
	* @return NONE
	*/
	public function get($uri='/', $params=false, $cookies=false)
	{
		// login to the server
		$handle = $this->login();
		
		// prepare uri for request
		if (empty($uri) || ($uri{0}!='/'))
		{
			$uri = "/$uri";
		}
				
		// check if parameters should be used and prepare them for request
		if (($params!=false) && (!empty($params)))
		{
			$params = "?$params";
		}
		
		else
		{
			$params = '';
		}
		
		// check if cookies should be used and prepare them for request
		if (($cookies!=false) && (!empty($cookies)))
		{
				$cookies = "Cookie: $cookies\r\n";
		}
		
		else
		{
			$cookies = '';
		}
		
		// prepare host-data
		$host = $this->host;
		if ($this->port != 80)
		{
			$host .= ':'.$this->port;
		}
		
		// send request to the server
		$this->sendCmd($handle, "GET $uri"."$params HTTP/1.1\r\n");
		$this->sendCmd($handle, "Host: $host\r\n$cookies");
		$this->sendCmd($handle, "Connection: close\r\n\r\n");
		
		// get server response
		$reply = $this->readSock($handle);
		
		// end connection to the server
		$this->logout($handle);
		
		// prepare headers and file
		$data = $this->divideReply($reply);
		
		return $data;
	}
			
	/**
	* log() - Saves all request to the server and their responses into $log
	*
	* @access: private
	* @param Str $str
	* @return NONE
	*/
	private function log($str)
	{
		if($this->logStats)
		{
			$this->log .= $str;
		}
	}
			
	/**
	* getLog() - Prints out all requests to the server and their responses 
	*
	* @access: public
	* @return NONE
	*/
	public function getLog()
	{
		return $this->log;
	}
}

try 
{
	// change Content-Type
	header('Content-Type: text/plain'); 
	$http = new HttpConnect("www.avedo.net", 80, true);
	
	$data = $http->head();
	
	$data2 = $http->get("httptest/test.php", "num=45");
	
	echo $http->getLog();
	
	print_r ($data);
	
	print_r ($data2);
}

catch(Exception $e) 
{
	echo $e->getMessage();
}
?>
 
Hat keiner ein bisschen Ahnung vom RFC 1945 und vom RFC 2616? Kann mir niemand bei diesem Problem weiter helfen?
MfG, Andy
 
Probier mal folgenden Algorithmus:
PHP:
private function parseResponse( $_plainResponse )
{
	if( !preg_match('/^HTTP\/(1\.[01]) ([1-5][0-9]{2})/', $_plainResponse, $match) ) {
		return false;
	}
	$hb = strpos($_plainResponse, "\r\n")+2;
	$cb = strpos($_plainResponse, "\r\n\r\n")+4;
	$_parsedResponse = array(
		'HTTP_VERSION'  => $match[1],
		'STATUS_CODE'   => $match[2],
		'HEADER_FIELDS' => array(),
		'CONTENT'       => (string) substr($_plainResponse, $cb+2),
	);
	$headerFields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', substr($_plainResponse, $hb, $cb-$hb-4)));
	foreach( $headerFields as $headerField ) {
		if( preg_match('/([^:]+):(.+)/m', $headerField, $match) ) {
			$_parsedResponse['HEADER_FIELDS'][_canonicalizeFieldName($match[1])] = trim($match[2]);
		}
	}
	return $_parsedResponse;
}
Die _canonicalizeFieldName()-Funktion ist dabei wie folgt definiert:
PHP:
function _canonicalizeFieldName( $string )
{
	return preg_replace('/(?<=^|[\x09\x20\x2D])./e', 'strtoupper("\0")', strtolower(trim($string)));
}
 
Morgen!
Vielen Dank für eure Hilfe. Werde Gumbos Script sofort mal ausprobieren, wenn ich zuhause bin. Sieht aber schon sehr vielversprechend aus. Werde die Klasse hoffentlich bald fertigstellen, sodass ich auch die Repository-Klasse vollenden kann.
MfG, Andy
 
Hallo!
Also erst nochmal vielen Dank an dich Gumbo für deine schnelle und souveräne Hilfe. Habe die Methode in bezug auf die Variabel- und Methoden-Namen, sowie die Einteilung etwas an mein Coding angepasst. Dokumentieren werde ich das wohl auch noch. Die Methode funktioniert eigentlich ganz gut. Der Content-Type steht nun auch im Array. Vorallem die Idee die HTTP-Version und den Status-Code als eigenständige Punkte zu nehmen finde ich sehr gut. Irgendwo war zwar ein Komma zuviel, aber es hat trotzdem funktioniert. Leider habe ich weiterhin ein kleines Problem mit dem Content-Element des Arrays. Am Anfang ist nun zwar der Buchstabe weg, allerdings steht weiterhin eine Null gefolgt von zwei Absätzen am Ende des Elements. Kann mir da vielleicht noch jemand helfen? Habe zwar mal etwas genauer im RFC gelesen, aber nichts dazu gefunden.
MfG, Andy

//EDIT: Ein Beispiel Aufruf (erst head() dann get())
 
Zuletzt bearbeitet von einem Moderator:
Hallo!
Erstmal möchte ich mich für die vielen Doppelpostings entschuldigen, aber ich habe es auch als nicht sehr sinnvoll erachtet einen neuen Tread für ein ähnliches Problem zu öffnen.

Die Klasse ist eigentlich fast fertig, doch leider besteht weiterhin ein kleiner Fehler, wobei ich nun endlich weiß wo der Fehler liegt und was ihn verursacht, wobei ich allerdings nicht genau weiß, wie man ihn beheben kann. Der Fehler im Content-Bereich entsteht durch das im RFC 2616 beschriebene Chunked Transfer Coding. Welchen Sinn diese Codierung hat bzw. wie ich sie wieder weg bekomme verstehe ich leider nicht ganz. Einen Teil scheint Gumbo in seinem Codeschnipsel schon behoben zu haben und zwar den Teil, der vor dem Content steht. Leider gibt es noch einen zweiten Teil, der den Content-Block abschließt. Wenn ich das richtig verstanden habe ist das einfach nur eine NULL gefolgt von einer Leerzeile. Sollte das richtig sein, ließe sich dieser Fehler ja sehr schnell beheben, allerdings stellt sich mir die Frage, ob das alles ist, oder ob es da noch andere kleine Änderungen im Content durch diese Codierung gibt. Bin für jede Hilfe sehr dankbar.

Des weiteren möchte ich mich nochmals bei Gumbo für diesen Codeschnipsel bedanken. Ein Danke auch an Denis Wronka, dessen Tutorial mir zwar nicht wirklich geholfen hat, mich allerdings darauf hinwies, dass die Übermittlung des User-Agents von vielen Servern erwartet wird. Danke euch beiden.

Wäre echt Dankbar, wenn mir jemand helfen könnte. Ich verzweifel gerade ein bisschen an diesem Chunked Transfer Coding.
MfG, Andy
 
Zuletzt bearbeitet von einem Moderator:
Leider gibt es noch einen zweiten Teil, der den Content-Block abschließt. Wenn ich das richtig verstanden habe ist das einfach nur eine NULL gefolgt von einer Leerzeile.
Ja das siehst du richtig, der Abschluss wird durch einen null Zeichen langen Teil gekennzeichnet. Eine Chunked-Nachricht könnte beispielsweise wie folgt aussehen:
Code:
Content-Type: text/plain
Transfer-Encoding: chunked

7
Hallo 
6
Welt!
0

.
(Den Punkt musst du dir wegdenken. Der wird benötigt, da Leerraumzeichen vom Forum entfernt werden.)
 
Also müsste am Ende des Content-Blocks "0\r\n\r\n" stehen. Das heißt ich suche einfach diesen String und ersetze ihn durch "". Oder? Zudem habe ich mir gerade nochmal deinen Code-Schnipsel angeschaut, steige aber leider nicht ganz durch, was daran liegen mag, das ich RegExp leider bisher sehr vernachlässigt habe. Entfernt dieser Code-Schnipsel denn den Rest, den diese Codierung veranstaltet? In deinem Beispiel wäre das ja "6\r\n" und "7\r\n".
MfG, Andy
 

Neue Beiträge

Zurück