Commits

Ludovic Chabant committed ab52036

Added support for multiple clients.

  • Participants
  • Parent commits 31c1cd8

Comments (0)

Files changed (6)

File lib/StupidHttp/Driver.php

     const REQUEST_LOG_FORMAT = "[%date%] %client_ip% --> %method% %path% --> %status% %status_name% [%time%ms]";
 
     protected $options;
-    protected $connection;
+    protected $connections;
 
     // Driver Properties {{{
     protected $server;
     public function run($options)
     {
         $this->options = $options;
-        $this->connection = false;
+        $this->connections = array();
         $this->handler->setLog($this->log);
         do
         {
         }
         while (true);
     }
+
+    public function runLimited($options, $loopCount = 1)
+    {
+        $this->options = $options;
+        $this->connections = array();
+        $this->handler->setLog($this->log);
+        while ($loopCount > 0)
+        {
+            $this->runOnce();
+            --$loopCount;
+        }
+    }
     // }}}
 
     // Secondary Methods {{{
     /**
      * Runs one request.
      */
-    public function runOnce()
+    protected function runOnce()
     {
-        // Establish a new connection if needed.
-        if ($this->connection === false)
+        // Check for active or new connections.
+        try
         {
-            $this->log->debug('Establishing connection...');
-            $this->connection = $this->handler->connect($this->options);
+            $ready = $this->handler->connect($this->connections, $this->options);
+            if ($ready == null)
+                return;
+        }
+        catch (Exception $e)
+        {
+            $this->log->error($e->getMessage());
+            return;
         }
 
-        // Receive a new request.
-        $requestInfo = $this->readRequest($this->connection);
-        if (!$requestInfo['error'])
+        // Add new connections to our list.
+        foreach ($ready as $c)
         {
-            $response = $this->processRequest($requestInfo);
-            $this->sendResponse($response, $requestInfo);
+            if (!in_array($c, $this->connections))
+                $this->connections[] = $c;
         }
+        $this->log->debug(count($ready) . "/" . count($this->connections) . " active connections.");
 
-        // Logging...
-        $this->logRequest($requestInfo);
-            
-        // Close the connection if it's OK to do so, or if the request was invalid.
-        if ($requestInfo['close_socket'] or $requestInfo['error'])
+        // Read from each active connection.
+        foreach ($ready as $c)
         {
-            $this->log->debug("Closing connection.");
-            $this->handler->disconnect($this->connection);
-            $this->connection = false;
+            try
+            {
+                // Receive a new request.
+                $requestInfo = $this->readRequest($c);
+                if (!$requestInfo['error'])
+                {
+                    $response = $this->processRequest($requestInfo);
+                    $this->sendResponse($response, $requestInfo);
+                }
+
+                // Logging...
+                $this->logRequest($requestInfo);
+
+                // Close the connection if it's OK to do so, or if the request was invalid.
+                if ($requestInfo['close_socket'] or $requestInfo['error'])
+                {
+                    $this->log->debug("Closing connection.");
+                    $this->handler->disconnect($c);
+                    $i = array_search($c, $this->connections);
+                    unset($this->connections[$i]);
+                }
+            }
+            catch (Exception $e)
+            {
+                $this->log->error($e->getMessage());
+            }
         }
     }
 
         $rawBody = false;
         $error = false;
         $profiling = array();
+        $closeSocket = false;
         try
         {
             // Start profiling.
 
             // Read the request header.
             $rawRequestStr = $this->handler->readUntil($connection, "\r\n\r\n");
-            $rawRequest = explode("\r\n", $rawRequestStr);
+            if ($rawRequestStr !== false)
+            {
+                $rawRequest = explode("\r\n", $rawRequestStr);
 
-            // Figure out if there's a body.
-            $contentLength = -1;
-            foreach ($rawRequest as $line)
-            {
-                $m = array();
-                if (preg_match('/^Content\-Length\:\s*(\d+)\s*$/', $line, $m))
+                // Figure out if there's a body.
+                $contentLength = -1;
+                foreach ($rawRequest as $line)
                 {
-                    $contentLength = (int)$m[1];
+                    $m = array();
+                    if (preg_match('/^Content\-Length\:\s*(\d+)\s*$/', $line, $m))
+                    {
+                        $contentLength = (int)$m[1];
+                    }
                 }
-            }
-            if ($contentLength > 0)
-            {
-                // Read the body chunk.
-                $rawBody = $this->handler->read($connection, $contentLength);
-            }
-        }
-        catch (StupidHttp_TimedOutException $e)
-        {
-            // Kept-alive connection probably timed out. Just close it.
-            if ($rawRequest === false)
-            {
-                $this->log->debug("Timed out... ending conversation.");
+                if ($contentLength > 0)
+                {
+                    // Read the body chunk.
+                    $rawBody = $this->handler->read($connection, $contentLength);
+                }
             }
             else
             {
-                $this->log->error("Timed out while receiving request.");
+                // The client closed the connection.
+                $closeSocket = true;
+                $error = true; // This is to bypass all processing.
             }
-            $error = true;
         }
         catch (StupidHttp_NetworkException $e)
         {
             'body' => $rawBody,
             'request' => null,
             'response' => null,
-            'close_socket' => true
+            'socket' => $connection,
+            'close_socket' => $closeSocket
         );
     }
 
         {
             $responseInfo = array();
             $responseStr = $this->buildRawResponse($response, $responseInfo);
-            $transmitted = $this->handler->write($this->connection, $responseStr);
+            $transmitted = $this->handler->write($requestInfo['socket'], $responseStr);
             $responseInfo['transmitted'] = $transmitted;
             $this->checkTransmittedResponse($response, $responseInfo);
         }
         $statusName = StupidHttp_WebServer::getHttpStatusHeader($response->getStatus());
 
         $responseStr = "HTTP/1.1 " . $statusName . PHP_EOL;
-        $responseStr .= "Server: PieCrust Chef Server".PHP_EOL;
+        $responseStr .= "Server: StupidHttp".PHP_EOL;
         $responseStr .= "Date: " . date("D, d M Y H:i:s T") . PHP_EOL;
         foreach ($response->getFormattedHeaders() as $header)
         {
         $response = $requestInfo['response'];
         if ($request and $response)
         {
-            $clientInfo = $this->handler->getClientInfo($this->connection);
+            $clientInfo = $this->handler->getClientInfo($requestInfo['socket']);
             $statusName = StupidHttp_WebServer::getHttpStatusHeader($response->getStatus());
             $replacements = array(
                 '%date%' => date(self::REQUEST_DATE_FORMAT),

File lib/StupidHttp/NetworkHandler.php

      *
      * The returned resource should be unique to the client connection.
      */
-    public function connect($options)
+    public function connect(array $connections, $options)
     {
         return -1;
     }

File lib/StupidHttp/SocketNetworkHandler.php

     }
 
     /**
-     * Waits for a new client connection, and returns the connection resource.
+     * Waits for incoming connections.
      */
-    public function connect($options)
+    public function connect(array $connections, $options)
     {
-        if (($msgsock = @socket_accept($this->sock)) === false)
+        $dummy = array();
+        $connections[] = $this->sock;
+        $ready = @socket_select(
+            $connections,
+            $dummy,
+            $dummy,
+            $options['poll_interval']
+        );
+        if ($ready === false)
         {
-            throw new StupidHttp_WebException("Failed accepting connection: " . socket_strerror(socket_last_error($this->sock)));
+            throw new StupidHttp_NetworkException("Failed to monitor incoming connections.");
         }
-        
-        $timeout = array('sec' => $options['timeout'], 'usec' => 0);
-        if (@socket_set_option($msgsock, SOL_SOCKET, SO_RCVTIMEO, $timeout) === false)
+        if ($ready == 0)
         {
-            throw new StupidHttp_WebException("Failed setting timeout value: " . socket_strerror(socket_last_error($msgsock)));
+            return null;
         }
 
-        return $msgsock;
+        // Check for a new connection.
+        $i = array_search($this->sock, $connections);
+        if ($i !== false)
+        {
+            // Remove our socket from the connections and replace it
+            // with the file-descriptor for the new client.
+            unset($connections[$i]);
+
+            if (($msgsock = @socket_accept($this->sock)) === false)
+            {
+                throw new StupidHttp_WebException(
+                    "Failed accepting connection: " .
+                    socket_strerror(socket_last_error($this->sock))
+                );
+            }
+
+            if (@socket_set_option($msgsock, SOL_SOCKET, SO_REUSEADDR, 1) === false)
+            {
+                throw new StupidHttp_WebException(
+                    "Failed setting address re-use option: " .
+                    socket_strerror(socket_last_error($msgsock))
+                );
+            }
+
+            $timeout = array('sec' => $options['timeout'], 'usec' => 0);
+            if (@socket_set_option($msgsock, SOL_SOCKET, SO_RCVTIMEO, $timeout) === false)
+            {
+                throw new StupidHttp_WebException(
+                    "Failed setting timeout value: " .
+                    socket_strerror(socket_last_error($msgsock))
+                );
+            }
+
+            $connections[] = $msgsock;
+        }
+
+        return $connections;
     }
 
     /**
 
             $data .= $buf;
         }
+        if (strlen($data) == 0)
+            return false;
         return $data;
     }
 
     {
         if (false === ($buf = @socket_read($connection, $this->sockReceiveBufferSize)))
         {
-            if (socket_last_error($connection) === SOCKET_ETIMEDOUT)
-            {
-                throw new StupidHttp_TimedOutException();
-            }
-            else
-            {
-                throw new StupidHttp_NetworkException(socket_strerror(socket_last_error($connection)));
-            }
+            throw new StupidHttp_NetworkException(socket_strerror(socket_last_error($connection)));
         }
         return $buf;
     }

File lib/StupidHttp/WebServer.php

      */
     public function __destruct()
     {
-        $this->getLog()->info("Shutting server down...");
-        $this->driver->unregister();
+        if ($this->driver != null)
+        {
+            $this->getLog()->info("Shutting server down...");
+            $this->driver->unregister();
+        }
     }
     // }}}
     
                 'run_browser' => false,
                 'keep_alive' => false,
                 'timeout' => 4,
+                'poll_interval' => 1,
                 'show_banner' => true,
                 'name' => null
             ),

File tests/src/StupidHttp/Mock/NetworkHandler.php

     public $registered;
     public $connections;
     public $contents;
+    public $bucket;
 
-    public function __construct($contents = '')
+    public function __construct($contents = '', $connections = array())
     {
         $this->registered = false;
-        $this->connections = array();
         $this->contents = $contents;
+        if (!is_array($connections))
+            $connections = array($connections);
+        $this->connections = $connections;
+        $this->bucket = '';
     }
 
     public function register()
         $this->registered = false;
     }
 
-    public function connect($options)
+    public function connect(array $connections, $options)
     {
-        $c = time();
-        $this->connections += $c;
-        return $c;
+        return $this->connections;
     }
 
     public function getClientInfo($connection)
 
     public function disconnect($connection)
     {
-        $i = array_search($connection, $this->connections);
-        if ($i === false)
-            throw new Exception("Connection '{$connection}' is unknown.");
-        unset($this->connections[$i]);
     }
 
     public function write($connection, $data)
     {
+        $this->bucket .= $data;
         return strlen($data);
     }
 

File tests/src/StupidHttp/Tests/DriverTest.php

 <?php
 
+use org\bovigo\vfs\vfsStream;
+
+
 class DriverTest extends PHPUnit_Framework_TestCase
 {
-    public function createDriver($networkContents = null, $documentRoot = null, $log = null)
+    public function createDriver($networkContents = null, $mockConnections = array(), $documentRoot = null, $log = null)
     {
         $server = new StupidHttp_Mock_WebServer();
         $vfs = new StupidHttp_VirtualFileSystem($documentRoot);
-        $handler = new StupidHttp_Mock_NetworkHandler($networkContents);
+        $handler = new StupidHttp_Mock_NetworkHandler($networkContents, $mockConnections);
         $driver = new StupidHttp_Driver($server, $vfs, $handler, $log);
         $server->setDriver($driver);
         return $driver;
         $this->assertNull($driver->getPreprocessor());
         $driver->setPreprocessor(42);
     }
+
+    public function testSimpleVfsRequest()
+    {
+        vfsStream::setup('root', null, array(
+            'robots.txt' => 'ROBOTS WELCOME'
+        ));
+        $driver = $this->createDriver(
+            "GET /robots.txt HTTP/1.1\r\n\r\n",
+            array(1),
+            vfsStream::url("root")
+        );
+        $options = array(
+            'list_directories' => true,
+            'list_root_directory' => false, 
+            'run_browser' => false,
+            'keep_alive' => false,
+            'timeout' => 4,
+            'poll_interval' => 1,
+            'show_banner' => true,
+            'name' => null
+        );
+        $driver->runLimited($options);
+        $runTime = date("D, d M Y H:i:s T");
+
+        $handler = $driver->getNetworkHandler();
+        $actual = $handler->bucket;
+        $actualLines = explode("\n", $actual);
+
+        $this->assertEquals('HTTP/1.1 200 OK', $actualLines[0]);
+
+        $expectedBody = "ROBOTS WELCOME";
+        $expectedBodyHash = md5($expectedBody);
+        $expectedHeaders = array(
+            'Server: StupidHttp',
+            'Date: ' . $runTime,
+            'Content-Length: ' . strlen($expectedBody),
+            'Content-MD5: ' . base64_encode($expectedBodyHash),
+            'Content-Type: text/plain',
+            'ETag: ' . $expectedBodyHash,
+            'Last-Modified: ' . $runTime,
+            'Connection: close'
+        );
+        $i = 1;
+        while (true)
+        {
+            if (strlen($actualLines[$i]) == 0)
+                break;
+            $this->assertContains(
+                $actualLines[$i],
+                $expectedHeaders
+            );
+            ++$i;
+        }
+        $this->assertEquals(
+            $expectedBody,
+            $actualLines[$i + 1]
+        );
+        $this->assertEquals($i + 2, count($actualLines));
+    }
 }