 +      $this->excludes[]= $pattern;
 +      return $this;
 +    }
-+    
-+    /**
-+     * Sends a request via this proxy
++
++    /**
++     * Check whether a given URL is excluded
++     *
++     * @param   peer.URL url
++     * @return  boolean
++     */
++    public function isExcluded(URL $url) {
++      foreach ($this->excludes as $pattern) {
++        if (stristr($url->getHost(), $pattern)) return TRUE;
++      }
++      return FALSE;
++    }
++  }
++?>
+
+Property changes on: skeleton/peer/http/HttpProxy.class.php
+___________________________________________________________________
+Name: svn:executable
+   + *
+Name: svn:keywords
+   + Id
+
+Index: skeleton/peer/http/CurlHttpTransport.class.php
+===================================================================
+--- skeleton/peer/http/CurlHttpTransport.class.php	(revision 0)
++++ skeleton/peer/http/CurlHttpTransport.class.php	(revision 0)
+@@ -0,0 +1,62 @@
++<?php
++/* This class is part of the XP framework
++ *
++ * $Id$ 
++ */
++
++  uses('peer.http.HttpTransport', 'io.streams.MemoryInputStream');
++
++  /**
++   * Transport via curl functions
++   *
++   * @ext      curl
++   * @see      xp://peer.http.HttpConnection
++   * @purpose  Transport
++   */
++  class CurlHttpTransport extends HttpTransport {
++    protected
++      $handle = NULL;
++
++    /**
++     * Constructor
++     *
++     * @param   peer.URL url
++     */
++    public function __construct(URL $url) {
++      $this->handle= curl_init();
++      curl_setopt($this->handle, CURLOPT_HEADER, 1);
++      curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1); 
++      curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0);
++      curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, 0);
++    }
++  
++    /**
++     * Sends a request
 +     *
 +     * @param   peer.http.HttpRequest request
 +     * @param   int timeout default 60
 +     * @param   float connecttimeout default 2.0
 +     * @return  peer.http.HttpResponse response object
 +     */
-+    public function sendRequest(HttpRequest $request, $timeout= 60, $connecttimeout= 2.0) {
-+    
-+      // Check if this URL is excluded
-+      foreach ($this->excludes as $pattern) {
-+        if (!strstr($request->url->getHost(), $pattern)) continue;
-+        return $request->send($timeout, $connecttimeout);
++    public function send(HttpRequest $request, $timeout= 60, $connecttimeout= 2.0) {
++      $curl= curl_copy_handle($this->handle);
++      curl_setopt($curl, CURLOPT_URL, $request->url->getUrl());
++      curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $request->getRequestString());
++      curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
++      
++      if ($this->proxy && !$this->proxy->isExcluded($request->getUrl())) {
++        curl_setopt($curl, CURLOPT_PROXY, $this->proxy->host);
++        curl_setopt($curl, CURLOPT_PROXYPORT, $this->proxy->port);
 +      }
-+
-+      // Open connection to proxy server
-+      $this->socket->setTimeout($timeout);
 +      
-+      // Modify target, a proxy wants "GET http://example.com/ HTTP/X.X"
-+      $request->target= sprintf(
-+        '%s://%s%s%s',
-+        $request->url->getScheme(),
-+        $request->url->getHost(),
-+        $request->url->getPort() ? ':'.$request->url->getPort() : '',
-+        $request->url->getPath('/')
-+      );
-+
-+      // Write request and return response
-+      $this->socket->connect($connecttimeout) && $this->socket->write($request->getRequestString());
-+      return new HttpResponse($this->socket);
++      $response= curl_exec($curl);
++      curl_close($curl);
++
++      if (FALSE === $response) {
++        throw new IOException(sprintf('%d: %s', curl_errno($curl), curl_error($curl)));
++      }
++
++      return new HttpResponse(new MemoryInputStream($response));
 +    }
 +  }
 +?>
+
+Property changes on: skeleton/peer/http/CurlHttpTransport.class.php
+___________________________________________________________________
+Name: svn:keywords
+   + Id
+
+Index: skeleton/peer/SocketInputStream.class.php
+===================================================================
+--- skeleton/peer/SocketInputStream.class.php	(revision 0)
++++ skeleton/peer/SocketInputStream.class.php	(revision 0)
+@@ -0,0 +1,72 @@
++<?php
++/* This socket is part of the XP framework's experiments
++ *
++ * $Id$
++ */
++
++  uses('io.streams.InputStream', 'peer.Socket');
++
++  /**
++   * InputStream that reads from a socket
++   *
++   * @purpose  InputStream implementation
++   */
++  class SocketInputStream extends Object implements InputStream {
++    protected
++      $socket= NULL;
++    
++    /**
++     * Constructor
++     *
++     * @param   peer.Socket socket
++     */
++    public function __construct(Socket $socket) {
++      $this->socket= $socket;
++      $this->socket->isConnected() || $this->socket->connect();
++    }
++
++    /**
++     * Read a string
++     *
++     * @param   int limit default 8192
++     * @return  string
++     */
++    public function read($limit= 8192) {
++      return $this->socket->read($limit);
++    }
++
++    /**
++     * Returns the number of bytes that can be read from this stream 
++     * without blocking.
++     *
++     */
++    public function available() {
++      return $this->socket->eof() ? -1 : 1;
++    }
++
++    /**
++     * Close this buffer
++     *
++     */
++    public function close() {
++      $this->socket->close();
++    }
++
++    /**
++     * Destructor. Ensures socket is closed.
++     *
++     */
++    public function __destruct() {
++      $this->socket->isConnected() && $this->close();
++    }
++
++    /**
++     * Creates a string representation of this socket
++     *
++     * @return  string
++     */
++    public function toString() {
++      return $this->getClassName().'<'.$this->socket->toString().'>';
++    }
++  }
++?>
+
+Property changes on: skeleton/peer/SocketInputStream.class.php
+___________________________________________________________________
+Name: svn:keywords
+   + Id
+
+Index: ports/classes/net/xp_framework/unittest/peer/HttpResponseTest.class.php
+===================================================================
+--- ports/classes/net/xp_framework/unittest/peer/HttpResponseTest.class.php	(revision 0)
++++ ports/classes/net/xp_framework/unittest/peer/HttpResponseTest.class.php	(revision 0)
+@@ -0,0 +1,79 @@
++<?php
++/* This class is part of the XP framework
++ *
++ * $Id$
++ */
++
++  uses(
++    'unittest.TestCase',
++    'peer.http.HttpResponse',
++    'io.streams.MemoryInputStream'
++  );
++
++  /**
++   * TestCase for HTTP responses
++   *
++   * @see      xp://peer.http.HttpResponse
++   * @purpose  Unittest
++   */
++  class HttpResponseTest extends TestCase {
++  
++    /**
++     * Get a response with the specified headers and body
++     *
++     * @param   string[] headers
++     * @param   string body default ''
++     * @return  peer.http.HttpResponse
++     */
++    protected function newResponse(array $headers, $body= '') {
++      return new HttpResponse(new MemoryInputStream(implode("\r\n", $headers)."\r\n\r\n".$body));
++    }
++  
++    /**
++     * Test empty response
++     *
++     */
++    #[@test]
++    public function getEmptyDocument() {
++      $response= $this->newResponse(array('HTTP/1.0 200 OK', 'Content-Length: 0'));
++      $this->assertEquals(200, $response->getStatusCode());
++      $this->assertEquals('0', $response->getHeader('Content-Length'));
++    }
++
++    /**
++     * Test non-empty response
++     *
++     */
++    #[@test]
++    public function getDocument() {
++      $body= '<h1>File not found</h1>';
++      $response= $this->newResponse(array('HTTP/1.0 404 OK', 'Content-Length: 23', 'Content-Type: text/html'), $body);
++      $this->assertEquals(404, $response->getStatusCode());
++      $this->assertEquals('23', $response->getHeader('Content-Length'));
++      $this->assertEquals('text/html', $response->getHeader('Content-Type'));
++      $this->assertEquals($body, $response->readData());
++    }
++
++    /**
++     * Test chunked transfer-encoding
++     *
++     */
++    #[@test]
++    public function getChunkedDocument() {
++      $body= '<h1>File not found</h1>';
++      $response= $this->newResponse(array('HTTP/1.0 404 OK', 'Transfer-Encoding: chunked'), "17\r\n".$body."\r\n0\r\n");
++      $this->assertEquals(404, $response->getStatusCode());
++      $this->assertEquals('chunked', $response->getHeader('Transfer-Encoding'));
++      $this->assertEquals($body, $response->readData());
++    }
++
++    /**
++     * Test what happens when the server responds with an incorrect protocol
++     *
++     */
++    #[@test, @expect('lang.FormatException')]
++    public function incorrectProtocol() {
++      $this->newResponse(array('* OK IMAP server ready H mimap20 68140'));
++    }
++  }
++?>
+
+Property changes on: ports/classes/net/xp_framework/unittest/peer/HttpResponseTest.class.php
+___________________________________________________________________
+Name: svn:keywords
+   + Id
+
+Index: ports/unittest/http.ini
+===================================================================
+--- ports/unittest/http.ini	(revision 12288)
++++ ports/unittest/http.ini	(working copy)
+@@ -8,3 +8,6 @@
+ 
+ [request]
+ class="net.xp_framework.unittest.peer.HttpRequestTest"
++
++[response]
++class="net.xp_framework.unittest.peer.HttpResponseTest"

       
       // Which HTTP method? GET and HEAD use query string, POST etc. use
       // body for passing parameters
@@ -144,10 +146,7 @@
       
       // Add request headers
       foreach ($this->headers as $k => $v) {

+     * @param   int port default 8080
+     */
+    public function __construct($host, $port= 8080) {
+      $this->host= $host;
+      $this->port= $port;
+    }
+
+    /**
