Made the webserver a bit smarter. It handles running as root properly (only allows it if user/group specified and port < 1024) and directory listing is massively smarter.
authorDan
Mon, 30 Jun 2008 12:36:13 -0400
changeset 21 74edc873234f
parent 20 bb8237ca678d
child 22 3a4f0cd0794e
Made the webserver a bit smarter. It handles running as root properly (only allows it if user/group specified and port < 1024) and directory listing is massively smarter.
ajax.php
greyhound.php
playlist.php
scripts/ajax.js
webserver-icons/app.png
webserver-icons/file.png
webserver-icons/folder.png
webserver.php
--- a/ajax.php	Fri Apr 25 14:56:52 2008 -0400
+++ b/ajax.php	Mon Jun 30 12:36:13 2008 -0400
@@ -34,9 +34,32 @@
   return true;
 }
 
-function ajax_request_handler($httpd)
+function ajax_request_handler($httpd, $socket)
 {
   global $playlist, $mime_types, $json, $allowcontrol;
+  global $use_auth, $auth_data;
+  
+  if ( $use_auth )
+  {
+    if ( !isset($_SERVER['PHP_AUTH_USER']) )
+    {
+      $httpd->header('WWW-Authenticate: basic');
+      $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect.");
+      return true;
+    }
+    if ( !isset($auth_data[$_SERVER['PHP_AUTH_USER']]) )
+    {
+      $httpd->header('WWW-Authenticate: basic');
+      $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect.");
+      return true;
+    }
+    else if ( $_SERVER['PHP_AUTH_PW'] !== $auth_data[$_SERVER['PHP_AUTH_USER']] )
+    {
+      $httpd->header('WWW-Authenticate: basic');
+      $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect.");
+      return true;
+    }
+  }
   
   // Set content type
   $httpd->header("Content-type: {$mime_types['js']}");
@@ -117,8 +140,11 @@
         rebuild_playlist();
       }
       $current_track = dcop_action('playlist', 'getActiveIndex');
+      $current_time = dcop_action('player', 'trackCurrentTime');
+      $is_playing = dcop_action('player', 'isPlaying');
       $return = array(
-          'is_playing' => dcop_action('player', 'isPlaying'),
+          'is_playing' => $is_playing,
+          'is_paused' => $current_time > 0 && !$is_playing,
           'current_track' => $current_track,
           'volume' => dcop_action('player', 'getVolume'),
           // include the MD5 of the playlist so that if it changes, the
@@ -131,7 +157,7 @@
       if ( isset($playlist[$current_track]) )
       {
         $return['current_track_length'] = $playlist[$current_track]['length_int'];
-        $return['current_track_pos'] = dcop_action('player', 'trackCurrentTime');
+        $return['current_track_pos'] = $current_time;
         $return['current_track_title'] = $playlist[$current_track]['title'];
         $return['current_track_artist'] = $playlist[$current_track]['artist'];
         $return['current_track_album'] = $playlist[$current_track]['album'];
--- a/greyhound.php	Fri Apr 25 14:56:52 2008 -0400
+++ b/greyhound.php	Mon Jun 30 12:36:13 2008 -0400
@@ -40,7 +40,17 @@
 // Allow forking when an HTTP request is received. This has advantages
 // and disadvantages. If this experimental option is enabled, it will
 // result in faster responses and load times but more memory usage.
-$allow_fork = true;
+$allow_fork = false;
+// set to true to enable authentication
+// WARNING: THIS HAS SOME SERIOUS SECURITY PROBLEMS RIGHT NOW. I don't
+// know what's causing it to not prompt for authentication from any
+// client after the first successful auth.
+$use_auth = false;
+// valid users and passwords
+$auth_data = array(
+    'funky' => 'monkey',
+    'fast' => 'forward'
+  );
 
 @ini_set('display_errors', 'on');
 
--- a/playlist.php	Fri Apr 25 14:56:52 2008 -0400
+++ b/playlist.php	Mon Jun 30 12:36:13 2008 -0400
@@ -13,9 +13,32 @@
  * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
  */
 
-function amarok_playlist($server)
+function amarok_playlist($httpd, $socket)
 {
   global $theme, $playlist, $allowcontrol;
+  global $use_auth, $auth_data;
+  
+  if ( $use_auth )
+  {
+    if ( !isset($_SERVER['PHP_AUTH_USER']) )
+    {
+      $httpd->header('WWW-Authenticate: basic');
+      $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect.");
+      return true;
+    }
+    if ( !isset($auth_data[$_SERVER['PHP_AUTH_USER']]) )
+    {
+      $httpd->header('WWW-Authenticate: basic');
+      $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect.");
+      return true;
+    }
+    else if ( $auth_data[$_SERVER['PHP_AUTH_USER']] !== $_SERVER['PHP_AUTH_PW'] )
+    {
+      $httpd->header('WWW-Authenticate: basic');
+      $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect.");
+      return true;
+    }
+  }
   
   $iphone = ( ( strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') ||
        strpos($_SERVER['HTTP_USER_AGENT'], 'iPod') ||
--- a/scripts/ajax.js	Fri Apr 25 14:56:52 2008 -0400
+++ b/scripts/ajax.js	Mon Jun 30 12:36:13 2008 -0400
@@ -187,7 +187,7 @@
         updateTitle(response.current_track_artist, response.current_track_album, response.current_track_title);
         
         // if not playing, set the position slider to zero
-        if ( !is_playing )
+        if ( !is_playing && !response.is_paused )
         {
           posslide_set_position(0);
         }
Binary file webserver-icons/app.png has changed
Binary file webserver-icons/file.png has changed
Binary file webserver-icons/folder.png has changed
--- a/webserver.php	Fri Apr 25 14:56:52 2008 -0400
+++ b/webserver.php	Mon Jun 30 12:36:13 2008 -0400
@@ -21,6 +21,14 @@
 define('HTTPD_VERSION', '0.1b1');
 
 /**
+ * Webserver system icons
+ */
+
+define('HTTPD_ICON_SCRIPT', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGSSURBVCjPVVFNSwJhEF78Ad79Cf6PvXQRsotUlzKICosuRYmR2RJR0KE6lBFFZVEbpFBSqKu2rum6llFS9HHI4iUhT153n6ZtIWMOM+/MM88z7wwH7s9Ub16SJcnbmrNcxVm2q7Z8/QPvEOtntpj92NkCqITLepEpjix7xQtiLOoQ2b6+E7YAN/5nfOEJ2WbKqOIOJ4bYVMEQx4LfBBQDsvFMhUcCVU1/CxVXmDBGA5ZETrhDCQVcYAPbyEJBhvrnBVPiSpNr6cYDNCQwo4zzU/ySckkgDYuNuVpI42T9k4gLKGMPs/xPzzovQiY2hQYe0jlJfyNNhTqiWDYBq/wBMcSRpnyPzu1oS7WtxjVBSthU1vgVksiQ3Dn6Gp5ah2YOKQo5GiuHPA6xT1EKpxQNCNYejgIR457KKio0S56YckjSa9jo//3mrj+BV0QQagqGTOo+Y7gZIf1puP3WHoLhEb2PjTlCTCWGXtbp8DCX3hZuOdaIc9A+aQvWk4ihq95p67a7nP+u+Ws+r0dql9z/zv0NCYhdCPKZ7oYAAAAASUVORK5CYII=');
+define('HTTPD_ICON_FOLDER', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGrSURBVDjLxZO7ihRBFIa/6u0ZW7GHBUV0UQQTZzd3QdhMQxOfwMRXEANBMNQX0MzAzFAwEzHwARbNFDdwEd31Mj3X7a6uOr9BtzNjYjKBJ6nicP7v3KqcJFaxhBVtZUAK8OHlld2st7Xl3DJPVONP+zEUV4HqL5UDYHr5xvuQAjgl/Qs7TzvOOVAjxjlC+ePSwe6DfbVegLVuT4r14eTr6zvA8xSAoBLzx6pvj4l+DZIezuVkG9fY2H7YRQIMZIBwycmzH1/s3F8AapfIPNF3kQk7+kw9PWBy+IZOdg5Ug3mkAATy/t0usovzGeCUWTjCz0B+Sj0ekfdvkZ3abBv+U4GaCtJ1iEm6ANQJ6fEzrG/engcKw/wXQvEKxSEKQxRGKE7Izt+DSiwBJMUSm71rguMYhQKrBygOIRStf4TiFFRBvbRGKiQLWP29yRSHKBTtfdBmHs0BUpgvtgF4yRFR+NUKi0XZcYjCeCG2smkzLAHkbRBmP0/Uk26O5YnUActBp1GsAI+S5nRJJJal5K1aAMrq0d6Tm9uI6zjyf75dAe6tx/SsWeD//o2/Ab6IH3/h25pOAAAAAElFTkSuQmCC');
+define('HTTPD_ICON_FILE', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAINSURBVBgZBcG/r55zGAfg6/4+z3va01NHlYgzEfE7MdCIGISFgS4Gk8ViYyM2Mdlsko4GSf8Do0FLRCIkghhYJA3aVBtEz3nP89wf11VJvPDepdd390+8Nso5nESBQoq0pfvXm9fzWf19453LF85vASqJlz748vInb517dIw6EyYBIIG49u+xi9/c9MdvR//99MPPZ7+4cP4IZhhTPbwzT2d+vGoaVRRp1rRliVvHq+cfvM3TD82+7mun0o/ceO7NT+/4/KOXjwZU1ekk0840bAZzMQ2mooqh0A72d5x/6sB9D5zYnff3PoYBoWBgFKPKqDKqjCpjKr//dcu9p489dra88cydps30KswACfNEKanSaxhlntjJ8Mv12Paie+vZ+0+oeSwwQ0Iw1xAR1CiFNJkGO4wu3ZMY1AAzBI0qSgmCNJsJUEOtJSMaCTBDLyQ0CknAGOgyTyFFiLI2awMzdEcSQgSAAKVUmAeNkxvWJWCGtVlDmgYQ0GFtgg4pNtOwbBcwQy/Rife/2yrRRVI0qYCEBly8Z+P4qMEMy7JaVw72N568e+iwhrXoECQkfH91kY7jwwXMsBx1L93ZruqrK6uuiAIdSnTIKKPLPFcvay8ww/Hh+ufeznTXu49v95IMoQG3784gYXdTqvRmqn/Wpa/ADFX58MW3L71SVU9ETgEIQQQIOOzub+fhIvwPRDgeVjWDahIAAAAASUVORK5CYII=');
+
+/**
  * Simple web server written in PHP.
  * @package Amarok
  * @subpackage WebControl
@@ -60,6 +68,13 @@
   var $default_document = false;
   
   /**
+   * List of filenames or handlers used when a directory listing is requested
+   * @var array
+   */
+  
+  var $directory_index = array('index.html', 'index.htm', 'index', 'default.html', 'default.htm');
+  
+  /**
    * HTTP response code set by the handler function
    * @var int
    */
@@ -110,12 +125,42 @@
   var $in_keepalive = false;
   
   /**
+   * UUID for this server instance
+   * @var string
+   */
+  
+  var $uuid = '00000000-0000-0000-0000-000000000000';
+  
+  /**
+   * Switch to track whether a scriptlet is running. If it is, send_http_error() does more than normal.
+   * @var bool
+   */
+  
+  var $in_scriptlet = false;
+  
+  /**
+   * Switch to track whether headers have been sent or not.
+   * @var bool
+   */
+  
+  var $headers_sent = false;
+  
+  /**
+   * Switch to track if the socket is bound and thus needs to be freed or not
+   * @var bool
+   */
+  
+  var $socket_initted = false;
+  
+  /**
    * Constructor.
    * @param string IPv4 address to bind to
    * @param int Port number
+   * @param int If port is under 1024, specify a user ID/name to switch to here
+   * @param int If port is under 1024, specify a group ID/name to switch to here
    */
   
-  function __construct($address = '127.0.0.1', $port = 8080)
+  function __construct($address = '127.0.0.1', $port = 8080, $targetuser = null, $targetgroup = null)
   {
     @set_time_limit(0);
     @ini_set('memory_limit', '256M');
@@ -126,17 +171,90 @@
       burnout('System does not support socket functions. Please rebuild your PHP or install an appropriate extension.');
     }
     
-    $this->sock = socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp'));
+    // make sure we're not running as root
+    // note that if allow_root is true, you must specify a UID/GID (or user/group) to switch to once the socket is bound
+    $allow_root = ( $port < 1024 ) ? true : false;
+    if ( function_exists('posix_geteuid') )
+    {
+      $euid = posix_geteuid();
+      $egid = posix_getegid();
+      $username = posix_getpwuid($euid);
+      $username = $username['name'];
+      $group = posix_getgrgid($egid);
+      $group = $group['name'];
+      if ( $euid == 0 && !$allow_root )
+      {
+        // running as root but not on a privileged port - die for security
+        burnout("Running as superuser (user \"$username\" and group \"$group\"). This is not allowed for security reasons.");
+      }
+      else if ( $euid == 0 && $allow_root )
+      {
+        // running as root and port below 1024, so notify of the switch and verify that a target UID and GID were passed
+        if ( $targetuser === null || $targetgroup === null )
+        {
+          // no target user/group specified
+          burnout("Must specify a target user and group when running server as root");
+        }
+        // get info about target user/group
+        if ( is_string($targetuser) )
+        {
+          $targetuser = posix_getpwnam($targetuser);
+          $targetuser = $targetuser['uid'];
+        }
+        if ( is_string($targetgroup) )
+        {
+          $targetgroup = posix_getgrnam($targetgroup);
+          $targetgroup = $targetgroup['uid'];
+        }
+        // make sure all info is valid
+        if ( !is_int($targetuser) || !is_int($targetgroup) )
+        {
+          burnout('Invalid user or group specified');
+        }
+        $userinfo = posix_getpwuid($targetuser);
+        $groupinfo = posix_getgrgid($targetgroup);
+        if ( function_exists('status') )
+          status("Will switch to user \"{$userinfo['name']}\" and group \"{$groupinfo['name']}\" shortly after binding to socket");
+      }
+      else if ( $allow_root && $euid > 0 )
+      {
+        burnout("Must be superuser to bind to ports below 1024");
+      }
+    }
+    $socket_do_root = ( $allow_root ) ? function_exists('posix_geteuid') : false;
+    
+    $this->sock = @socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp'));
     if ( !$this->sock )
       throw new Exception('Could not create socket');
-    $result = socket_bind($this->sock, $address, $port);
+    $result = @socket_bind($this->sock, $address, $port);
     if ( !$result )
       throw new Exception("Could not bind to $address:$port");
-    $result = socket_listen($this->sock, SOMAXCONN);
+    $this->socket_initted = true;
+    $result = @socket_listen($this->sock, SOMAXCONN);
     if ( !$result )
       throw new Exception("Could not listen for connections $address:$port");
+    
+    // if running as root and we made it here, switch credentials
+    if ( $socket_do_root )
+    {
+      posix_setuid($targetuser);
+      posix_setgid($targetgroup);
+      posix_setegid($targetgroup);
+      posix_seteuid($targetuser);
+      if ( function_exists('status') )
+        status('Successfully switched user ID');
+    }
+    
     $this->bind_address = $address;
     $this->server_string = "PhpHttpd/" . HTTPD_VERSION . " PHP/" . PHP_VERSION . "\r\n";
+    
+    // create a UUID
+    $uuid_base = md5(microtime() . ( function_exists('mt_rand') ? mt_rand() : rand() ));
+    $this->uuid = substr($uuid_base, 0,  8) . '-' .
+                  substr($uuid_base, 8,  4) . '-' .
+                  substr($uuid_base, 12, 4) . '-' .
+                  substr($uuid_base, 16, 4) . '-' .
+                  substr($uuid_base, 20, 20);
   }
   
   /**
@@ -145,9 +263,15 @@
   
   function __destruct()
   {
-    if ( !defined('HTTPD_WS_CHILD') )
+    if ( !defined('HTTPD_WS_CHILD') && $this->socket_initted )
     {
-      status('WebServer: destroying socket');
+      if ( function_exists('status') )
+        status('WebServer: destroying socket');
+      // http://us3.php.net/manual/en/function.socket-bind.php
+      if ( !@socket_set_option($this->sock, SOL_SOCKET, SO_REUSEADDR, 1) )
+      {
+        echo socket_strerror(socket_last_error($sock)) . "\n";
+      }
       @socket_shutdown($this->sock, 2);
       @socket_close($this->sock);
     }
@@ -164,7 +288,8 @@
       // if this is a child process, we're finished - close up shop
       if ( defined('HTTPD_WS_CHILD') && !$this->in_keepalive )
       {
-        status('Exiting child process');
+        if ( function_exists('status') )
+          status('Exiting child process');
         @socket_shutdown($remote);
         @socket_close($remote);
         exit(0);
@@ -211,11 +336,14 @@
         {
           // this is the child
           define('HTTPD_WS_CHILD', 1);
+          @socket_set_option($this->sock, SOL_SOCKET, SO_REUSEADDR, 1);
           socket_close($this->sock);
         }
       }
       
       $this->in_keepalive = false;
+      $this->headers_sent = false;
+      $this->in_scriptlet = false;
       
       // read request
       $last_line = '';
@@ -261,6 +389,11 @@
         $this->in_keepalive = ( strtolower($_SERVER['HTTP_CONNECTION']) === 'keep-alive' );
       }
       
+      // parse authorization, if any
+      if ( isset($_SERVER['PHP_AUTH_USER']) )
+      {
+        unset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
+      }
       if ( isset($_SERVER['HTTP_AUTHORIZATION']) )
       {
         $data = $_SERVER['HTTP_AUTHORIZATION'];
@@ -270,6 +403,7 @@
         $_SERVER['PHP_AUTH_PW'] = substr(strstr($data, ':'), 1);
       }
       
+      // anything on POST?
       $postdata = '';
       $_POST = array();
       if ( $method == 'POST' )
@@ -303,8 +437,11 @@
         $uri    = substr($uri, 0, strpos($uri, '?'));
       }
       
+      // set some server vars
       $_SERVER['REQUEST_URI'] = '/' . rawurldecode($uri);
       $_SERVER['REQUEST_METHOD'] = $method;
+      
+      // get remote IP and port
       socket_getpeername($remote, $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT']);
       
       $_GET = array();
@@ -319,31 +456,60 @@
         }
       }
       
+      // init handler
+      $handler = false;
+      
       if ( $uri == '' )
       {
+        // user requested the root (/). If there's a default document, use that; else, see if we can do a directory listing
         $uri = strval($this->default_document);
+        if ( !$this->default_document && $this->allow_dir_list )
+        {
+          // we can list directories and this was requested by the user, so list it out
+          $handler = array('type' => 'rootdir');
+        }
       }
       
       $uri_parts = explode('/', $uri);
       
+      // hook for the special UUID handler
+      if ( $uri_parts[0] === $this->uuid && !$handler )
+      {
+        $handler = array('type' => 'sysuuid');
+      }
+      
       // loop through URI parts, see if a handler is set
-      $handler = false;
-      for ( $i = count($uri_parts) - 1; $i >= 0; $i-- )
+      if ( !$handler )
       {
-        $handler_test = implode('/', $uri_parts);
-        if ( isset($this->handlers[$handler_test]) )
+        for ( $i = count($uri_parts) - 1; $i >= 0; $i-- )
         {
-          $handler = $this->handlers[$handler_test];
-          $handler['id'] = $handler_test;
-          break;
+          $handler_test = implode('/', $uri_parts);
+          if ( isset($this->handlers[$handler_test]) )
+          {
+            $handler = $this->handlers[$handler_test];
+            $handler['id'] = $handler_test;
+            break;
+          }
+          unset($uri_parts[$i]);
         }
-        unset($uri_parts[$i]);
       }
       
       if ( !$handler )
       {
-        $this->send_http_error($remote, 404, "The requested URL /$uri was not found on this server.");
-        continue;
+        // try to make a fakie
+        if ( $this->check_for_handler_children($uri) )
+        {
+          $handler = array(
+            'type' => 'folder',
+            'dir' => "/{$this->uuid}/__fakie",
+            'id' => $uri
+          );
+        }
+        if ( !$handler )
+        {
+          $this->send_http_error($remote, 404, "The requested URL /$uri was not found on this server.");
+          continue;
+        }
       }
       
       $this->send_standard_response($remote, $handler, $uri, $params);
@@ -354,7 +520,8 @@
         //   status('Closing connection');
         @socket_shutdown($remote);
         @socket_close($remote);
-        status('Exiting child process');
+        if ( function_exists('status') )
+          status('Exiting child process');
         exit(0);
       }
       else if ( defined('HTTPD_WS_CHILD') )
@@ -363,6 +530,11 @@
         //   status('Continuing connection');
         // @socket_write($remote, "\r\n\r\n");
       }
+      else
+      {
+        @socket_shutdown($remote);
+        @socket_close($remote);
+      }
     }
   }
   
@@ -377,10 +549,18 @@
   function send_client_headers($socket, $http_code = 200, $contenttype = 'text/html', $headers = '')
   {
     global $http_responses;
+    if ( $this->headers_sent )
+      return false;
+
+    // this is reset after the request is completed (hopefully)    
+    $this->headers_sent = true;
+    
     $reason_code = ( isset($http_responses[$http_code]) ) ? $http_responses[$http_code] : 'Unknown';
     
     $_SERVER['HTTP_USER_AGENT'] = ( isset($_SERVER['HTTP_USER_AGENT']) ) ? $_SERVER['HTTP_USER_AGENT'] : '(no user agent)';
-    status("{$_SERVER['REMOTE_ADDR']} {$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']} $http_code {$_SERVER['HTTP_USER_AGENT']}");
+    
+    if ( function_exists('status') )
+      status("{$_SERVER['REMOTE_ADDR']} {$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']} $http_code {$_SERVER['HTTP_USER_AGENT']}");
     
     $headers = str_replace("\r\n", "\n", $headers);
     $headers = str_replace("\n", "\r\n", $headers);
@@ -408,7 +588,7 @@
   {
     switch ( $handler['type'] )
     {
-      case 'dir':
+      case 'folder':
         // security
         $uri = str_replace("\000", '', $_SERVER['REQUEST_URI']);
         if ( preg_match('#(\.\./|\/\.\.)#', $uri) || strstr($uri, "\r") || strstr($uri, "\n") )
@@ -420,58 +600,55 @@
         global $mime_types;
         
         // trim handler id from uri
+        $uri_full = rtrim($uri, '/');
         $uri = substr($uri, strlen($handler['id']) + 1);
         
         // get file path
         $file_path = rtrim($handler['dir'], '/') . $uri;
-        if ( file_exists($file_path) )
+        if ( file_exists($file_path) || $this->check_for_handler_children($uri_full) )
         {
           // found it :-D
           
           // is this a directory?
-          if ( is_dir($file_path) )
+          if ( is_dir($file_path) || $this->check_for_handler_children($uri_full) )
           {
+            // allowed to list?
             if ( !$this->allow_dir_list )
             {
               $this->send_http_error($socket, 403, "Directory listing is not allowed.");
               return true;
             }
             // yes, list contents
-            $root = '/' . $handler['id'] . rtrim($uri, '/');
-            $parent = substr($root, 0, strrpos($root, '/')) . '/';
-              
+            try
+            {
+              $dir_list = $this->list_directory($uri_full, true);
+            }
+            catch ( Exception $e )
+            {
+              $this->send_http_error($socket, 500, "Directory listing failed due to an error in the listing core method. This may indicate that the webserver process does not have filesystem access to the specified directory.<br /><br />Debugging details:<pre>$e</pre>");
+              return true;
+            }
+            
+            $root = rtrim($uri_full, '/') . '/';
+            $parent = rtrim(dirname(rtrim($uri_full, '/')), '/') . '/';
+            
             $contents = <<<EOF
 <html>
   <head>
     <title>Index of: $root</title>
+    <link rel="stylesheet" type="text/css" href="/{$this->uuid}/dirlist.css" />
   </head>
   <body>
     <h1>Index of $root</h1>
     <ul>
-      <li><a href="$parent">Parent directory</a></li>
+      <li><tt><a href="$parent">Parent directory</a></tt></li>
     
 EOF;
-            $dirs = array();
-            $files = array();
-            $d = @opendir($file_path);
-            while ( $dh = readdir($d) )
+
+            foreach ( $dir_list as $filename => $info )
             {
-              if ( $dh == '.' || $dh == '..' )
-                continue;
-              if ( is_dir("$file_path/$dh") )
-                $dirs[] = $dh;
-              else
-                $files[] = $dh;
-            }
-            asort($dirs);
-            asort($files);
-            foreach ( $dirs as $dh )
-            {
-              $contents .= '  <li><a href="' . $root . '/' . $dh . '">' . $dh . '/</a></li>' . "\n    ";
-            }
-            foreach ( $files as $dh )
-            {
-              $contents .= '  <li><a href="' . $root . '/' . $dh . '">' . $dh . '</a></li>' . "\n    ";
+              $ts = ( $info['type'] == 'folder' ) ? '/' : '';
+              $contents .= '  <li><tt><a href="' . htmlspecialchars($root . basename($filename) . $ts) . '"><img alt="[   ]" src="/' . $this->uuid . '/' . $info['type'] . '.png" /> ' . htmlspecialchars($filename) . $ts . '</a></tt></li>' . "\n    ";
             }
             $contents .= "\n    </ul>\n    <address>Served by {$this->server_string}</address>\n</body>\n</html>\n\n";
             
@@ -567,7 +744,7 @@
         }
         
         break;
-      case 'function':
+      case 'script':
         // init vars
         $this->content_type = 'text/html';
         $this->response_code = 200;
@@ -578,7 +755,9 @@
         try
         {
           ob_start();
-          $result = @call_user_func($handler['function'], $this);
+          $this->in_scriptlet = true;
+          $result = @call_user_func($handler['function'], $this, $socket);
+          $this->in_scriptlet = false;
           $output = ob_get_contents();
           ob_end_clean();
         }
@@ -586,7 +765,8 @@
         {
           restore_error_handler();
           $this->send_http_error($socket, 500, "A handler crashed with an exception; see the command line for details.");
-          status("caught exception in handler {$handler['id']}:\n$e");
+          if ( function_exists('status') )
+            status("caught exception in handler {$handler['id']}:\n$e");
           return true;
         }
         restore_error_handler();
@@ -610,6 +790,150 @@
         // write body
         @socket_write($socket, $output);
         
+        $this->headers_sent = false;
+        
+        break;
+      case 'sysuuid':
+        // requested one of the system's icon images
+        $uri_parts = explode('/', $_SERVER['REQUEST_URI']);
+        if ( count($uri_parts) != 3 )
+        {
+          $this->send_http_error($socket, 404, "The requested URL " . htmlspecialchars($_SERVER['REQUEST_URI']) . " was not found on this server.");
+        }
+        
+        // load image data
+        $filename =& $uri_parts[2];
+        switch ( $filename )
+        {
+          case 'script.png':
+            ( !isset($image_data) ) ? $image_data = HTTPD_ICON_SCRIPT : null;
+          case 'folder.png':
+            ( !isset($image_data) ) ? $image_data = HTTPD_ICON_FOLDER : null;
+          case 'file.png':
+            ( !isset($image_data) ) ? $image_data = HTTPD_ICON_FILE : null;
+            
+            $image_data = base64_decode($image_data);
+            $found = true;
+            $type = 'image/png';
+            break;
+          case 'dirlist.css':
+            $type = 'text/css';
+            $found = true;
+            $image_data = <<<EOF
+/**
+ * PhpHttpd directory list visual style
+ */
+
+html {
+  background-color: #c9c9c9;
+  margin: 0;
+  padding: 0;
+}
+
+body {
+  background-color: #ffffff;
+  margin: 20px;
+  padding: 10px;
+  border: 1px solid #aaaaaa;
+}
+
+a {
+  text-decoration: none;
+}
+
+a img {
+  border-width: 0;
+}
+
+ul {
+  list-style-type: none;
+}
+
+EOF;
+            break;
+          default:
+            $found = false;
+        }
+        
+        // ship it out
+        if ( $found )
+        {
+          $lm_date = date('r', filemtime(__FILE__));
+          $size = strlen($image_data);
+          $this->send_client_headers($socket, 200, $type, "Last-Modified: $lm_date\r\nContent-Length: $size");
+          @socket_write($socket, $image_data);
+        }
+        else
+        {
+          $this->send_http_error($socket, 404, "The requested URL " . htmlspecialchars($_SERVER['REQUEST_URI']) . " was not found on this server.");
+        }
+        
+        return true;
+        
+        break;
+      case 'rootdir':
+        //
+        // list the contents of the document root
+        //
+        
+        $handlers = $this->list_directory('/', true);
+        
+        $contents = <<<EOF
+<html>
+  <head>
+    <title>Index of: /</title>
+    <link rel="stylesheet" type="text/css" href="/{$this->uuid}/dirlist.css" />
+  </head>
+  <body>
+    <h1>Index of /</h1>
+    <ul>
+    
+EOF;
+        
+        $html = '';
+        // generate content
+        foreach ( $handlers as $uri => $handler )
+        {
+          switch($handler['type'])
+          {
+            case 'folder':
+              $image = 'folder.png';
+              $abbr = 'DIR';
+              $add = '/';
+              break;
+            case 'file':
+            default:
+              $image = 'file.png';
+              $abbr = '   ';
+              $add = '';
+              break;
+            case 'script':
+              $image = 'script.png';
+              $abbr = 'CGI';
+              $add = '';
+              break;
+          }
+          $html .= "  <li><tt><a href=\"/$uri\"><img alt=\"[{$abbr}]\" src=\"/{$this->uuid}/{$image}\" /> {$uri}{$add}</a></tt></li>\n      ";
+        }
+        
+        $contents .= $html;
+        $contents .= <<<EOF
+</ul>
+    <address>Served by {$this->server_string}</address>
+  </body>
+</html>
+EOF;
+
+        // get length
+        $len = strlen($contents);
+        
+        // send headers
+        $this->send_client_headers($socket, 200, 'text/html', "Content-Length: $len");
+        
+        // write to the socket
+        @socket_write($socket, $contents);
+        
+        return true;
         break;
     }
   }
@@ -646,7 +970,14 @@
   {
     global $http_responses;
     $reason_code = ( isset($http_responses[$http_code]) ) ? $http_responses[$http_code] : 'Unknown';
-    $this->send_client_headers($socket, $http_code);
+    
+    // if we're in a scriptlet, include custom headers
+    if ( $this->in_scriptlet )
+      $headers = implode("\r\n", $this->response_headers);
+    else
+      $headers = '';
+      
+    $this->send_client_headers($socket, $http_code, 'text/html', $headers);
     $html = <<<EOF
 <html>
   <head>
@@ -661,8 +992,7 @@
 </html>
 EOF;
     @socket_write($socket, $html);
-    @socket_close($socket);
-  }
+  } 
   
   /**
    * Adds a new handler
@@ -673,11 +1003,15 @@
   
   function add_handler($uri, $type, $value)
   {
+    if ( $type == 'dir' )
+      $type = 'folder';
+    if ( $type == 'function' )
+      $type = 'script';
     switch($type)
     {
-      case 'dir':
+      case 'folder':
         $this->handlers[$uri] = array(
-            'type' => 'dir',
+            'type' => 'folder',
             'dir' => $value
           );
         break;
@@ -687,9 +1021,9 @@
             'file' => $value
           );
         break;
-      case 'function':
+      case 'script':
         $this->handlers[$uri] = array(
-            'type' => 'function',
+            'type' => 'script',
             'function' => $value
           );
         break;
@@ -709,6 +1043,265 @@
     echo '</div>';
   }
   
+  /**
+   * Lists out the contents of a directory, including virtual handlers.
+   * @example
+   * Example return data: (will be ksorted)
+   <code>
+   array(
+       'bar' => 'folder',
+       'baz' => 'script',
+       'foo' => 'file'
+     );
+   </code>
+   * @param string Directory name, relative to the server's document root
+   * @param bool If true, sorts folders first (default: false)
+   * @return array Exception thrown on failure
+   */
+  
+  function list_directory($dir, $folders_first = false)
+  {
+    // clean slashes from the directory name
+    $dir = trim($dir, '/');
+    
+    if ( $dir == '' )
+    {
+      //
+      // list the root, which can consist only of handlers
+      //
+      
+      // copy the handlers array, which we need to ksort
+      $handlers = $this->handlers;
+      
+      // get rid of multi-depth handlers
+      foreach ( $handlers as $uri => $handler )
+      {
+        if ( strpos($uri, '/') )
+        {
+          unset($handlers[$uri]);
+          $newuri = explode('/', $uri);
+          if ( !isset($handlers[$newuri[0]]) )
+          {
+            $handlers[$newuri[0]] = array(
+                'type' => 'folder'
+              );
+          }
+        }
+      }
+      
+      ksort($handlers);
+      
+      if ( $folders_first )
+      {
+        // sort folders first
+        $handlers_sorted = array();
+        foreach ( $handlers as $uri => $handler )
+        {
+          if ( $handler['type'] == 'folder' )
+            $handlers_sorted[$uri] = $handler;
+        }
+        foreach ( $handlers as $uri => $handler )
+        {
+          if ( $handler['type'] != 'folder' )
+            $handlers_sorted[$uri] = $handler;
+        }
+        $handlers = $handlers_sorted;
+        unset($handlers_sorted);
+      }
+      
+      // done
+      return $handlers;
+    }
+    else
+    {
+      // list something within the root
+      $dir_stack = explode('/', $dir);
+      
+      // lookup handler
+      $handler_search = $dir;
+      $found_handler = false;
+      $fake_handler = false;
+      $i = 1;
+      while ( $i > 0 )
+      {
+        if ( isset($this->handlers[$handler_search]) )
+        {
+          $found_handler = true;
+          break;
+        }
+        $i = strrpos($handler_search, '/');
+        $handler_search = substr($handler_search, 0, strrpos($handler_search, '/'));
+      }
+      if ( $this->check_for_handler_children($dir) )
+      {
+        $fake_handler = true;
+      }
+      else if ( !$found_handler )
+      {
+        // nope. not there.
+        throw new Exception("ERR_NO_SUCH_FILE_OR_DIRECTORY");
+      }
+      
+      // make sure this is a directory
+      if ( !$fake_handler )
+      {
+        $handler =& $handler_search;
+        if ( $this->handlers[$handler]['type'] != 'folder' )
+        {
+          throw new Exception("ERR_NOT_A_DIRECTORY");
+        }
+        
+        // determine real path
+        $real_path = realpath($this->handlers[$handler]['dir'] . substr($dir, strlen($handler)));
+        
+        // directory is resolved; list contents
+        $dir_contents = array();
+        
+        if ( $dr = opendir($real_path) )
+        {
+          while ( $dh = readdir($dr) )
+          {
+            if ( $dh == '.' || $dh == '..' )
+            {
+              continue;
+            }
+            $dir_contents[$dh] = array(
+                'type' => ( is_dir("$real_path/$dh") ) ? 'folder' : 'file',
+                'size' => filesize("$real_path/$dh"),
+                'time' => filemtime("$real_path/$dh")
+              );
+          }
+        }
+        else
+        {
+          // only if directory open failed
+          throw new Exception("ERR_PERMISSION_DENIED");
+        }
+        
+        closedir($dr);
+      
+        // some cleanup
+        unset($handler, $handler_search);
+      }
+      
+      // list any additional handlers in there
+      foreach ( $this->handlers as $handler => $info )
+      {
+        // parse handler name
+        $handler_name = explode('/', trim($handler, '/'));
+        // is this handler in this directory?
+        if ( count($handler_name) != count($dir_stack) + 1 )
+        {
+          continue;
+        }
+        foreach ( $dir_stack as $i => $_ )
+        {
+          if ( $dir_stack[$i] != $handler_name[$i] )
+          {
+            continue 2;
+          }
+        }
+        // it's in here!
+        $dir_contents[ basename($handler) ] = array(
+            'type' => $info['type']
+          );
+      }
+      
+      // list "fake" handlers
+      foreach ( $this->handlers as $handler => $info )
+      {
+        // parse handler name
+        $handler_name = explode('/', trim($handler, '/'));
+        // is this handler somewhere underneath this directory?
+        if ( count($handler_name) < count($dir_stack) + 2 )
+        {
+          continue;
+        }
+        // path check
+        foreach ( $dir_stack as $i => $_ )
+        {
+          if ( $dir_stack[$i] != $handler_name[$i] )
+          {
+            continue 2;
+          }
+        }
+        // create a "fake" directory
+        $fakie_name = $handler_name[ count($dir_stack) ];
+        $dir_contents[$fakie_name] = array(
+            'type' => 'folder'
+          );
+      }
+      
+      if ( $folders_first )
+      {
+        // perform folder sorting
+        $unsorted = $dir_contents;
+        ksort($unsorted);
+        $dir_contents = array();
+        foreach ( $unsorted as $name => $info )
+        {
+          if ( $info['type'] == 'folder' )
+            $dir_contents[$name] = $info;
+        }
+        foreach ( $unsorted as $name => $info )
+        {
+          if ( $info['type'] != 'folder' )
+            $dir_contents[$name] = $info;
+        }
+      }
+      else
+      {
+        // not sorting with folders first, so just alphabetize
+        ksort($dir_contents);
+      }
+      
+      // done
+      
+      return $dir_contents;
+    }
+  }
+  
+  /**
+   * Searches deeper to see if there are sub-handlers within a path to see if a fake handler can be created
+   * @param string URI
+   * @return bool
+   */
+  
+  function check_for_handler_children($file_path)
+  {
+    $file_path = trim($file_path, '/');
+    $dir_stack = explode('/', $file_path);
+    
+    // make sure this isn't a "real" handler
+    if ( isset($this->handlers[$file_path]) )
+    {
+      return false;
+    }
+    
+    // list any additional handlers in there
+    foreach ( $this->handlers as $handler => $info )
+    {
+      // parse handler name
+      $handler_name = explode('/', trim($handler, '/'));
+      // is this handler in this directory?
+      if ( count($handler_name) != count($dir_stack) + 1 )
+      {
+        continue;
+      }
+      foreach ( $dir_stack as $i => $_ )
+      {
+        if ( $dir_stack[$i] != $handler_name[$i] )
+        {
+          continue 2;
+        }
+      }
+      // it's in here!
+      return true;
+    }
+    
+    return false;
+  }
+  
 }
 
 /**