webserver.php
author Dan
Tue, 26 May 2009 15:25:30 -0400
changeset 69 73780a159e15
parent 68 32f6e2ee15ab
child 76 487a16c7117c
permissions -rw-r--r--
DCOP: added safeguards for mysterious zombie behavior

<?php

/**
 * Webserver class
 * 
 * Greyhound - real web management for Amarok
 * Copyright (C) 2008 Dan Fuhry
 *
 * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
 */

require('multithreading.php');

/**
 * Version of the server
 * @const string
 */

define('HTTPD_VERSION', '0.1b5');

/**
 * Length of keep-alive connections
 * @const int
 */

define('HTTPD_KEEP_ALIVE_TIMEOUT', 300);

/**
 * 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=');

/**
 * Abstraction layer to use
 */

define('HTTPD_SOCKET_LAYER', 'Stream');

/**
 * Simple but full-featured embedded web server written in PHP.
 * @package Amarok
 * @subpackage WebControl
 * @author Dan Fuhry
 * @license GNU General Public License <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
 */

class WebServer
{
  
  /**
   * IP address we're bound to
   * @var string
   */
  
  var $bind_address = '127.0.0.1';
  
  /**
   * Port we're listening on
   * @var int
   */
   
  var $port = 8080;
  
  /**
   * Socket abstraction object
   * @var object
   */
  
  var $server = null;
  
  /**
   * Server string
   * @var string
   */
  
  var $server_string = 'PhpHttpd';
  
  /**
   * Default document (well default handler)
   * @var string
   */
  
  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
   */
  
  var $response_code = 0;
    
  /**
   * Content type set by the current handler function
   * @var string
   */
  
  var $content_type = '';
  
  /**
   * Response headers to send back to the client
   * @var array
   */
  
  var $response_headers = array();
  
  /**
   * List of handlers
   * @var array
   */
   
  var $handlers = array();
  
  /**
   * Switch to control if directory listing is enabled
   * @var bool
   */
  
  var $allow_dir_list = false;
  
  /**
   * Switch to control forking support.
   * @var bool
   */
  
  var $allow_fork = true;
  
  /**
   * Multi-threading manager.
   * @var object
   */
  
  var $threader = false;
  
  /**
   * Keep-alive support uses this to track what the client requested.
   * Only used if $allow_fork is set to true.
   * @var bool
   */
  
  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;
  
  /**
   * The list of child processes spawned by this server.
   * @var array
   */
  
  var $child_list = array();
  
  /**
   * The parent process's PID
   * @var int
   */
  
  var $parent_pid = 0;
  
  /**
   * List of IPC request handlers
   * @var array
   * @access private
   */
  
  var $ipc_handlers = array();
  
  /**
   * Sockets for parent and child to communicate
   * @var resource
   * @var resource
   */
  
  var $parent_sock = null;
  var $child_sock = null;
  
  /**
   * Switched on when a graceful reboot event is sent.
   * @var bool
   */
  
  var $reboot_sent = 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, $targetuser = null, $targetgroup = null)
  {
    @set_time_limit(0);
    @ini_set('memory_limit', '128M');
    
    // 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['gid'];
        }
        // 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;
    
    $class = 'Socket_' . HTTPD_SOCKET_LAYER;
    $this->server = new $class();
    if ( is_array($address) )
    {
      foreach ( $address as $a )
      {
        if ( is_array($port) )
        {
          foreach ( $port as $p )
          {
            $this->server->tcp_listen($a, $p);
          }
        }
        else
        {
          $this->server->tcp_listen($a, $port);
        }
      }
    }
    else
    {
      if ( is_array($port) )
      {
        foreach ( $port as $p )
        {
          $this->server->tcp_listen($address, $p);
        }
      }
      else
      {
        $this->server->tcp_listen($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->port = $port;
    $this->server_string = "PhpHttpd/" . HTTPD_VERSION . " PHP/" . PHP_VERSION . "\r\n";
    $this->parent_pid = getmypid();
    $this->threader = new Threader();
    $this->threader->ipc_register('ws_reboot', array(&$this, 'reboot'));
    
    // 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);
                  
  }
  
  /**
   * Destructor.
   */
  
  function __destruct()
  {
    if ( !$this->threader )
    {
      return false;
    }
    if ( !$this->threader->is_child() && $this->socket_initted )
    {
      if ( function_exists('status') )
        status('WebServer: destroying socket');
      $this->server->destroy();
      
      // tell all children to shut down
      if ( $this->allow_fork )
      {
        if ( function_exists('status') )
          status('WebServer: asking all children to exit');
        $this->threader->kill_all_children();
      }
    }
  }
  
  /**
   * Reboot the server. Useful for applying new settings.
   * @param string Optional, new IP address to bind to
   * @param int Optional, new port to bind to
   * @param bool Optional, whether to allow forking or not
   */
  
  function reboot($addr = null, $port = null, $allow_fork = null)
  {
    if ( function_exists('status') )
      status('Reboot request has been received');
    
    $addr = ( !is_string($addr) ) ? $this->bind_address : $addr;
    $port = ( !is_int($port) ) ? $this->port : $port;
    $fork = ( !is_bool($allow_fork) ) ? $this->allow_fork : $allow_fork;
    
    //
    // REBOOTING IS A COMPLICATED THING.
    // We need to ask all children to close any existing connections so
    // that all relevant socket resources can be freed. Then we need to
    // call the constructor again to respawn the server, and finally
    // re-enter the server loop.
    //
    // However, reboot() is often called from a PHP-based handler. This
    // means that some config page probably still needs to be sent. What
    // we can do here is send an IPC event that fires the actual reboot,
    // then return to allow the current page to finish up. We also need
    // to signal the current process to shut down any existing keep-
    // alive connections. This can be accomplished by setting in_keepalive
    // to false.
    //
    
    // Kill the entire child after this response is sent
    $this->in_keepalive = false;
    
    // If we're the parent process, we need to know that a reboot event
    // was fired, and thus the server's main socket needs to be destroyed
    // and recreated. This is just done with another boolean switch.
    $this->reboot_sent = true;
    
    // this is really to track if there are any children
    $oldfork = $this->allow_fork;
    
    // Set our new server flags
    $this->bind_address = $addr;
    $this->port = $port;
    $this->allow_fork = $fork;
    
    // If we're a child, we have to tell the parent what the hell is
    // going on, and then get out of here as quickly as possible
    // (and other children should do the same). If this is a child,
    // fire an IPC reboot event. Else, fire a "die all" event
    if ( $this->threader->is_child() )
    {
      if ( function_exists('status') )
        status('Signaling parent with parameter changes (fork = ' . intval($fork) . ') + reboot request');
      // this is the child
      $this->threader->ipc_send(array(
        'action' => 'ws_reboot',
        'addr' => $addr,
        'port' => $port,
        'fork' => $fork
      ));
    }
    else if ( !$this->threader->is_child() && $oldfork )
    {
      // we are the parent and have been asked to respawn. there are (presumably)
      // still children running around; when one of them dies, we'll receive a
      // SIGCHLD, but often times this is already being called from the SIGUSR2
      // handler. whoops.
      if ( function_exists('status') )
        status('Waiting on all children');
      
      // this is the parent, and there are children present
      $this->threader->kill_all_children();
      
      // all children are dead, we are ok to respawn
      $this->respawn();
    }
    else
    {
      // not sure what to do in this particular scenario.
    }
  }
  
  /**
   * Respawns the server. All children should be dead, and any client
   * connections must be closed.
   */
  
  function respawn()
  {
    $this->reboot_sent = false;
    
    if ( function_exists('status') )
      status('Respawn event sent');
    $this->server->destroy();
    unset($this->server);
    
    // try to spawn up to 10 times
    for ( $i = 0; $i < 10; $i++ )
    {
      try
      {
        $this->__construct($this->bind_address, $this->port);
      }
      catch ( Exception $e )
      {
        if ( $i == 9 )
        {
          if ( function_exists('burnout') )
          {
            burnout("Couldn't respawn because one of the child processes did not die, and thus the port was not freed.");
          }
          exit(1);
        }
        if ( function_exists('status') )
        {
          status("Respawn failed, retrying in 2 seconds");
        }
        usleep(2000000);
        continue;
      }
      break;
    }
    
    if ( function_exists('status') )
      status('Respawn is complete, entering server loop with bind_address = ' . $this->bind_address . ' allow_fork = ' . strval(intval($this->allow_fork)));
    
    // all handlers should already be set up, so just break out and we should automatically continue the server loop
  }
  
  /**
   * Main server loop
   */
  
  function serve()
  {
    while ( true )
    {
      ##
      ## STAGE 0: CLEANUP FROM PREVIOUS RUN
      ##
      
      // if this is a child process, we're finished - close up shop
      if ( $this->threader->is_child() && !$this->in_keepalive )
      {
        if ( function_exists('status') )
          status('Exiting child process');
        
        $remote->destroy();
        
        exit(0);
      }
      
      ##
      ## STAGE 1: LISTENER AND INIT
      ##
      
      // wait for connection...
      if ( !$this->threader->is_child() )
      {
        $remote = $this->server->accept();
      }
      
      if ( !$remote )
      {
        $this->in_keepalive = false;
        continue;
      }
      
      // fork off if possible
      if ( function_exists('pcntl_fork') && $this->allow_fork && !$this->in_keepalive )
      {
        $fork_result = $this->threader->fork();
        if ( $fork_result == Threader::FORK_CHILD )
        {
          // this is the child
          define('HTTPD_WS_CHILD', 1);
        }
        else
        {
          // we are the parent, continue listening
          $remote->soft_shutdown();
          $this->child_list[] = $fork_result;
          continue;
        }
      }
      
      $this->in_keepalive = false;
      $this->headers_sent = false;
      $this->in_scriptlet = false;
      
      ##
      ## STAGE 2: READ REQUEST
      ##
      
      // this is a complicated situation because we need to keep enough ticks going to properly handle
      // signals, meaning we can't use stream_set_timeout() and instead need to rely on our own timing
      // logic. setting the timeout to a short period, say 200,000 usec, we can minimize CPU usage and
      // have a good response time.
      
      $remote->set_timeout(0, 200000);
      $start_time = microtime(true);
      $client_headers = '';
      $last_line = '';
      while ( true )
      {
        if ( $start_time + HTTPD_KEEP_ALIVE_TIMEOUT < microtime(true) || $remote->is_eof() )
        {
          // request expired -- end the iteration here
          if ( !$this->threader->is_child() )
            $remote->destroy();
          
          continue 2;
        }
        $line = strval($remote->read_normal());
        $line = str_replace("\r", "", $line);
        
        // raw layer wants to send 2 empty lines through, stream layer wants to send one.
        $last_line_check = ( HTTPD_SOCKET_LAYER != 'Raw' || ( HTTPD_SOCKET_LAYER == 'Raw' && $last_line == "\n" ) );
        if ( $line == "\n" && $last_line_check )
          // we have two newlines in a row, break out since we have a full request
          break;
          
        $client_headers .= $line;
        $last_line = $line;
      }
      
      ##
      ## STAGE 3: PARSE REQUEST AND HEADERS
      ##
      
      // parse request
      $client_headers = trim($client_headers);
      
      if ( isset($last_finish_time) && empty($client_headers) && $this->threader->is_child() && $last_finish_time + HTTPD_KEEP_ALIVE_TIMEOUT < microtime(true) )
      {
        status('[debug] keep-alive connection timed out (checkpoint 2)');
        continue; // will jump back to the start of the loop and kill the child process
      }
      
      $client_headers = explode("\n", $client_headers);
      
      // first line
      $request = $client_headers[0];
      if ( !preg_match('/^(GET|POST) \/([^ ]*) HTTP\/1\.[01]$/', $request, $match) )
      {
        $this->send_http_error($remote, 400, 'Your client issued a malformed or illegal request.');
        continue;
      }
      $method =& $match[1];
      $uri =& $match[2];
      
      // set client header SERVER variables
      foreach ( $_SERVER as $key => $_ )
      {
        if ( preg_match('/^HTTP_/', $key) )
          unset($_SERVER[$key]);
      }
      if ( !isset($_SERVER['SERVER_SOFTWARE']) )
      {
        $_SERVER['SERVER_SOFTWARE'] =& $this->server_string;
      }
      unset($client_headers[0]);
      foreach ( $client_headers as $line )
      {
        if ( !preg_match('/^([A-z0-9-]+): (.+)$/is', $line, $match) )
          continue;
        $key = 'HTTP_' . strtoupper(str_replace('-', '_', $match[1]));
        $_SERVER[$key] = $match[2];
      }
      
      // enable keep-alive if requested
      if ( isset($_SERVER['HTTP_CONNECTION']) && $this->threader->is_child() )
      {
        $this->in_keepalive = ( strtolower($_SERVER['HTTP_CONNECTION']) === 'keep-alive' );
      }
      
      // process cookies
      $_COOKIE = array();
      if ( isset($_SERVER['HTTP_COOKIE']) )
      {
        preg_match_all('/([a-z0-9_-]+)=([^;]*)(?:;|$)/', trim($_SERVER['HTTP_COOKIE']), $matches);
        foreach ( $matches[0] as $i => $match )
        {
          $_COOKIE[$matches[1][$i]] = str_replace('\\r', "\r", str_replace('\\n', "\n", str_replace(rawurlencode(';'), ';', $matches[2][$i])));
        }
      }
      
      // parse authorization, if any
      unset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
      if ( isset($_SERVER['HTTP_AUTHORIZATION']) )
      {
        $data = $_SERVER['HTTP_AUTHORIZATION'];
        $data = substr(strstr($data, ' '), 1);
        $data = base64_decode($data);
        $_SERVER['PHP_AUTH_USER'] = substr($data, 0, strpos($data, ':'));
        $_SERVER['PHP_AUTH_PW'] = substr(strstr($data, ':'), 1);
      }
      
      // anything on POST?
      $postdata = '';
      $_POST = array();
      $_FILES = array();
      if ( $method == 'POST' )
      {
        $this->parse_post_data($remote);
      }
      
      // parse URI
      $params = '';
      if ( strstr($uri, '?') )
      {
        $params = substr(strstr($uri, '?'), 1);
        $uri    = substr($uri, 0, strpos($uri, '?'));
      }
      
      // set some server vars
      $_SERVER['REQUEST_URI'] = '/' . rawurldecode($uri);
      $_SERVER['REQUEST_METHOD'] = $method;
      
      // get remote IP and port
      $remote->get_peer_info($_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT']);
      
      // process $_GET
      $_GET = array();
      if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]-]+)(=[^ &]+)?/', $params, $matches) )
      {
        if ( isset($matches[1]) )
        {
          foreach ( $matches[0] as $i => $_ )
          {
            $_GET[$matches[2][$i]] = ( !empty($matches[3][$i]) ) ? urldecode(substr($matches[3][$i], 1)) : true;
          }
        }
      }
      
      // Parse GET, POST, and FILES into multi-depth arrays
      $_GET = $this->parse_multi_depth_array($_GET);
      $_POST = $this->parse_multi_depth_array($_POST);
      $_FILES = $this->parse_multi_depth_array($_FILES);
      
      ##
      ## STAGE 4: HANDLER RESOLUTION
      ##
      
      // 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
      if ( !$handler )
      {
        for ( $i = count($uri_parts) - 1; $i >= 0; $i-- )
        {
          $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]);
        }
      }
      
      if ( !$handler )
      {
        // 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;
        }
      }
      
      ##
      ## STAGE 5: HANDLER CALL
      ##
      
      $this->send_standard_response($remote, $handler, $uri, $params);
      
      ##
      ## STAGE 6: CLEANUP
      ##
      
      // now that we're done sending the response, delete any temporary uploaded files
      if ( !empty($_FILES) )
      {
        foreach ( $_FILES as $file_data )
        {
          if ( file_exists($file_data['tmp_name']) )
          {
            @unlink($file_data['tmp_name']);
          }
        }
      }
      
      if ( !$this->in_keepalive && $this->threader->is_child() )
      {
        // connection: close
        // continue on to the shutdown handler
        continue;
      }
      else if ( $this->threader->is_child() )
      {
        // if ( $this->threader->is_child() )
        //   status('Continuing connection');
        // $remote->write("\r\n\r\n");
        $last_finish_time = microtime(true);
      }
      else
      {
        // standalone process
        $remote->destroy();
        
        // if a reboot was fired and we're running in single-process mode, now is the time to respawn
        if ( !$this->threader->is_child() && $this->reboot_sent )
        {
          $this->respawn();
        }
      }
    }
  }
  
  /**
   * Parse POST data and format $_POST and $_FILES.
   * @param resource Remote socket
   */
  
  function parse_post_data($remote)
  {
    $postdata = '';
    
    // read POST data
    if ( isset($_SERVER['HTTP_CONTENT_TYPE']) && preg_match('#^multipart/form-data; ?boundary=([A-z0-9_-]+)$#i', $_SERVER['HTTP_CONTENT_TYPE'], $match) )
    {
      // this is a multipart request
      $boundary =& $match[1];
      $mode = 'data';
      $last_line = '';
      $i = 0;
      while ( $data = $remote->read_normal(8388608) )
      {
        $data_trim = trim($data, "\r\n");
        if ( $mode != 'data' )
        {
          $data = str_replace("\r", '', $data);
        }
        if ( ( $data_trim === "--$boundary" || $data_trim === "--$boundary--" ) && $i > 0 )
        {
          // trim off the first LF and the last CRLF
          if ( HTTPD_SOCKET_LAYER == 'Raw' )
            $currval_data = substr($currval_data, 1, strlen($currval_data)-3);
          else
            $currval_data = substr($currval_data, 0, strlen($currval_data)-2);
          
          // this is the end of a part of the message; parse it into either $_POST or $_FILES
          if ( is_string($have_a_file) )
          {
            // write data to a temporary file
            $errcode = UPLOAD_ERR_OK;
            $tempfile = tempnam('phpupload', ( function_exists('sys_get_temp_dir') ? sys_get_temp_dir() : '/tmp' ));
            if ( $fh = @fopen($tempfile, 'w') )
            {
              if ( empty($have_a_file) )
              {
                $errcode = UPLOAD_ERR_NO_FILE;
              }
              else
              {
                fwrite($fh, $currval_data);
              }
              fclose($fh);
            }
            else
            {
              $errcode = UPLOAD_ERR_CANT_WRITE;
            }
            $_FILES[$currval_name] = array(
                'name' => $have_a_file,
                'type' => $currval_type,
                'size' => filesize($tempfile),
                'tmp_name' => $tempfile,
                'error' => $errcode
              );
          }
          else
          {
            if ( preg_match('/\[\]$/', $currval_name) )
            {
              if ( !isset($_POST[$currval_name]) || ( isset($_POST[$currval_name]) && !is_array($_POST[$currval_name]) ) )
                $_POST[$currval_name] = array();
              
              $_POST[$currval_name][] = $currval_data;
            }
            else
            {
              $_POST[$currval_name] = $currval_data;
            }
          }
        }
        
        if ( $data_trim === "--$boundary" )
        {
          // switch from "data" mode to "headers" mode
          $currval_name = '';
          $currval_data = '';
          $currval_type = '';
          $have_a_file = false;
          $mode = 'headers';
        }
        else if ( $data_trim === "--$boundary--" )
        {
          // end of request
          break;
        }
        else if ( ( empty($data_trim) && ( ( HTTPD_SOCKET_LAYER == 'Raw' && empty($last_line) ) || HTTPD_SOCKET_LAYER != 'Raw' ) ) && $mode == 'headers' )
        {
          // start of data
          $mode = 'data';
        }
        else if ( $mode == 'headers' )
        {
          // read header
          // we're only looking for Content-Disposition and Content-Type
          if ( preg_match('#^Content-Disposition: form-data; name="([^"\a\t\r\n]+)"(?:; filename="([^"\a\t\r\n]+)")?#i', $data_trim, $match) )
          {
            // content-disposition header, set name and mode.
            $currval_name = $match[1];
            if ( isset($match[2]) )
            {
              $have_a_file = $match[2];
            }
            else
            {
              $have_a_file = false;
            }
          }
          else if ( preg_match('#^Content-Type: ([a-z0-9-]+/[a-z0-9/-]+)$#i', $data_trim, $match) )
          {
            $currval_type = $match[1];
          }
        }
        else if ( $mode == 'data' )
        {
          $currval_data .= $data;
        }
        $last_line = $data_trim;
        $i++;
      }
    }
    else
    {
      if ( isset($_SERVER['HTTP_CONTENT_LENGTH']) )
      {
        $postdata = $remote->read_binary(intval($_SERVER['HTTP_CONTENT_LENGTH']));
      }
      else
      {
        $postdata = $remote->read_normal(8388608);
      }
      if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]%-]+)(=[^ &]+)?/i', $postdata, $matches) )
      {
        if ( isset($matches[1]) )
        {
          foreach ( $matches[0] as $i => $_ )
          {
            $currval_name =& $matches[2][$i];
            $currval_data = ( !empty($matches[3][$i]) ) ? urldecode(substr($matches[3][$i], 1)) : true;
            $currval_name = urldecode($currval_name);
            
            if ( preg_match('/\[\]$/', $currval_name) )
            {
              $basename = preg_replace('/\[\]$/', '', $currval_name);
              if ( !isset($_POST[$basename]) || ( isset($_POST[$basename]) && !is_array($_POST[$basename]) ) )
                $_POST[$basename] = array();
              
              $_POST[$basename][] = $currval_data;
            }
            else
            {
              $_POST[$currval_name] = $currval_data;
            }
          }
        }
      }
    }
  }
  
  /**
   * Sends the client appropriate response headers.
   * @param resource Socket connection to client
   * @param int HTTP status code, defaults to 200
   * @param string Content type, defaults to text/html
   * @param string Additional headers to send, optional
   */                     
  
  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)';
    
    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);
    $headers = preg_replace("#[\r\n]+$#", '', $headers);
    $connection = ( $this->in_keepalive ) ? 'keep-alive' : 'close';
    
    $socket->write("HTTP/1.1 $http_code $reason_code\r\n");
    $socket->write("Server: $this->server_string");
    $socket->write("Connection: $connection\r\n");
    $socket->write("Content-Type: $contenttype\r\n");
    if ( !empty($headers) )
    {
      $socket->write("$headers\r\n");
    }
    $socket->write("\r\n");
  }
  
  /**
   * Sends a normal response
   * @param resource Socket connection to client
   * @param array Handler
   */
  
  function send_standard_response($socket, $handler)
  {
    switch ( $handler['type'] )
    {
      case 'folder':
        // security
        $uri = str_replace("\000", '', $_SERVER['REQUEST_URI']);
        if ( preg_match('#(\.\./|\/\.\.)#', $uri) || strstr($uri, "\r") || strstr($uri, "\n") )
        {
          $this->send_http_error($socket, 403, 'Access to this resource is forbidden.');
        }
        
        // import mimetypes
        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) || $this->check_for_handler_children($uri_full) )
        {
          // found it :-D
          
          // is this a directory?
          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
            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><tt><a href="$parent">Parent directory</a></tt></li>
    
EOF;

            foreach ( $dir_list as $filename => $info )
            {
              $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";
            
            $sz = strlen($contents);
            $this->send_client_headers($socket, 200, 'text/html', "Content-length: $sz\r\n");
            
            $socket->write($contents);
            
            return true;
          }
          
          // try to open the file
          $fh = @fopen($file_path, 'r');
          if ( !$fh )
          {
            // can't open it, send a 404
            $this->send_http_error($socket, 404, "The requested URL " . htmlspecialchars($_SERVER['REQUEST_URI']) . " was not found on this server.");
          }
          
          // get size
          $sz = filesize($file_path);
          
          // mod time
          $time = date('r', filemtime($file_path));
          
          // all good, send headers
          $fileext = substr($file_path, strrpos($file_path, '.') + 1);
          $mimetype = ( isset($mime_types[$fileext]) ) ? $mime_types[$fileext] : 'application/octet-stream';
          $this->send_client_headers($socket, 200, $mimetype, "Content-length: $sz\r\nLast-Modified: $time\r\n");
          
          // send body
          while ( $blk = @fread($fh, 768000) )
          {
            $socket->write($blk);
          }
          fclose($fh);
          return true;
        }
        else
        {
          $this->send_http_error($socket, 404, "The requested URL " . htmlspecialchars($_SERVER['REQUEST_URI']) . " was not found on this server.");
        }
        
        break;
      case 'file':
        
        // import mimetypes
        global $mime_types;
        
        // get file path
        $file_path = $handler['file'];
        if ( file_exists($file_path) )
        {
          // found it :-D
          
          // is this a directory?
          if ( is_dir($file_path) )
          {
            $this->send_http_error($socket, 500, "Host script mapped a directory as a file entry.");
            return true;
          }
          
          // try to open the file
          $fh = @fopen($file_path, 'r');
          if ( !$fh )
          {
            // can't open it, send a 404
            $this->send_http_error($socket, 404, "The requested URL " . htmlspecialchars($_SERVER['REQUEST_URI']) . " was not found on this server.");
          }
          
          // get size
          $sz = filesize($file_path);
          
          // mod time
          $time = date('r', filemtime($file_path));
          
          // all good, send headers
          $fileext = substr($file_path, strrpos($file_path, '.') + 1);
          $mimetype = ( isset($mime_types[$fileext]) ) ? $mime_types[$fileext] : 'application/octet-stream';
          $this->send_client_headers($socket, 200, $mimetype, "Content-length: $sz\r\nLast-Modified: $time\r\n");
          
          // send body
          while ( $blk = @fread($fh, 768000) )
          {
            $socket->write($blk);
          }
          fclose($fh);
          return true;
        }
        else
        {
          $this->send_http_error($socket, 404, "The requested URL " . htmlspecialchars($_SERVER['REQUEST_URI']) . " was not found on this server.");
        }
        
        break;
      case 'script':
        // init vars
        $this->content_type = 'text/html';
        $this->response_code = 200;
        $this->response_headers = array();
        
        // error handling
        @set_error_handler(array($this, 'function_error_handler'), E_ALL);
        try
        {
          ob_start();
          $this->in_scriptlet = true;
          $result = @call_user_func($handler['function'], $this, $socket);
          $this->in_scriptlet = false;
          $output = ob_get_contents();
          ob_end_clean();
        }
        // throw an HttpExceptionFatal when you need to break out of an in-progress scriptlet due to an error, use it in place of die() or exit()
        catch ( HttpExceptionFatal $e )
        {
          ob_end_clean();
          restore_error_handler();
          $this->send_http_error($socket, 500, "A handler crashed reporting a fatal exception; see the command line for details.");
          if ( function_exists('status') )
            status("fatal exception in handler {$handler['id']}:\n$e");
          return true;
        }
        catch ( HttpSuccess $e )
        {
          // just finish with success
          $this->in_scriptlet = false;
          $output = ob_get_contents();
          ob_end_clean();
        }
        catch ( Exception $e )
        {
          ob_end_clean();
          restore_error_handler();
          $this->send_http_error($socket, 500, "There was an uncaught exception during the execution of a scripted handler function. See the command line for details.");
          if ( function_exists('status') )
            status("uncaught exception in handler {$handler['id']}:\n$e");
          return true;
        }
        
        restore_error_handler();
        
        // the handler function should return this magic string if it writes its own headers and socket data
        if ( $output == '__break__' )
        {
          return true;
        }
        
        if ( !$this->headers_sent )
        {
          // $this->header('Transfer-encoding: chunked');
          $this->header("Content-length: " . strlen($output));
          $headers = implode("\r\n", $this->response_headers);
          
          // write headers
          $this->send_client_headers($socket, $this->response_code, $this->content_type, $headers);
          
          // chunk output
          // $output = dechex(strlen($output)) . "\r\n$output";
          
          // write body
          if ( !empty($output) )
            $socket->write($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($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($contents);
        
        return true;
        break;
    }
  }
  
  /**
   * Adds an HTTP header value to send back to the client
   * @var string Header
   */
  
  function header($str)
  {
    if ( preg_match('#HTTP/1\.[01] ([0-9]+) (.+?)[\s]*$#', $str, $match) )
    {
      $this->response_code = intval($match[1]);
      return true;
    }
    else if ( preg_match('#Content-type: ([^ ;]+)#i', $str, $match) )
    {
      $this->content_type = $match[1];
      return true;
    }
    $this->response_headers[] = $str;
    return true;
  }
  
  /**
   * Sets a cookie. Identical to PHP's setcookie() function.
   * @param string Cookie name
   * @param string Cookie value
   * @param int Expiration time of cookie as a UNIX timestamp; if omitted or set to zero, cookie will expire at the end of the user's browser session
   * @param string Path of the cookie
   * @param string Domain the cookie is available under
   * @param bool If true, browser will only send the cookie through an HTTPS connection.
   * @param bool If true, cookie will not be accessible to client-side code
   */
  
  function setcookie($cookiename, $cookievalue, $expiry = false, $path = false, $domain = false, $secure = false, $httponly = false)
  {
    $header = "Set-Cookie: $cookiename=$cookievalue";
    if ( !empty($expiry) )
      $header .= "; expires=" . gmdate('D, d-M-Y H:i:s T', $expiry);
    if ( !empty($path) )
      $header .= "; path=$path";
    if ( !empty($domain) )
      $header .= "; domain=$domain";
    if ( $secure )
      $header .= "; secure";
    if ( $httponly )
      $header .= "; httponly";
    
    if ( is_int($expiry) && $expiry < time() )
    {
      unset($_COOKIE[$cookiename]);
    }
    else
    {
      $_COOKIE[$cookiename] = $cookievalue;
    }
    
    $this->header($header);
    return $header;
  }
  
  /**
   * Sends the client an HTTP error page
   * @param resource Socket connection to client
   * @param int HTTP status code
   * @param string Detailed error string
   */
  
  function send_http_error($socket, $http_code, $errstring)
  {
    global $http_responses;
    $reason_code = ( isset($http_responses[$http_code]) ) ? $http_responses[$http_code] : 'Unknown';
    
    // generate error page
    $html = <<<EOF
<html>
  <head>
    <title>$http_code $reason_code</title>
  </head>
  <body>
    <h1>$http_code $reason_code</h1>
    <p>$errstring</p>
    <hr />
    <address>Served by $this->server_string</address>
  </body>
</html>
EOF;
    
    // length of the response (required if we want keep-alive to work)
    $this->header('Content-length: ' . strlen($html));
    
    // if we're in a scriptlet, include custom headers
    if ( $this->in_scriptlet )
      $headers = implode("\r\n", $this->response_headers);
    else
      $headers = 'Content-length: ' . strlen($html);
      
    $this->send_client_headers($socket, $http_code, 'text/html', $headers);
    $socket->write($html);
  } 
  
  /**
   * Adds a new handler
   * @param string URI, minus the initial /
   * @param string Type of handler - function or dir
   * @param string Value - function name or absolute/relative path to directory
   */
  
  function add_handler($uri, $type, $value)
  {
    if ( $type == 'dir' )
      $type = 'folder';
    if ( $type == 'function' )
      $type = 'script';
    switch($type)
    {
      case 'folder':
        $this->handlers[$uri] = array(
            'type' => 'folder',
            'dir' => $value
          );
        break;
      case 'file':
        $this->handlers[$uri] = array(
            'type' => 'file',
            'file' => $value
          );
        break;
      case 'script':
        $this->handlers[$uri] = array(
            'type' => 'script',
            'function' => $value
          );
        break;
    }
  }
  
  /**
   * Error handling function
   * @param see <http://us.php.net/manual/en/function.set-error-handler.php>
   */
  
  function function_error_handler($errno, $errstr, $errfile, $errline, $errcontext)
  {
    echo '<div style="border: 1px solid #AA0000; background-color: #FFF0F0; padding: 10px;">';
    echo "<b>PHP warning/error:</b> type $errno ($errstr) caught in <b>$errfile</b> on <b>$errline</b><br />";
    // echo "Error context:<pre>" . htmlspecialchars(print_r($errcontext, true)) . "</pre>";
    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;
  }
  
  /**
   * Takes a flat array with keys of format foo[bar] and parses it into multiple depths.
   * @param array
   * @return array
   */
  
  function parse_multi_depth_array($array)
  {
    foreach ( $array as $key => $value )
    {
      if ( preg_match('/^([^\[\]]+)\[([^\]]*)\]/', $key, $match) )
      {
        $parent =& $match[1];
        $child =& $match[2];
        if ( !isset($array[$parent]) || ( isset($array[$parent]) && !is_array($array[$parent]) ) )
        {
          $array[$parent] = array();
        }
        if ( empty($child) )
        {
          $array[$parent][] = $value;
        }
        else
        {
          $array[$parent][$child] = $value;
        }
        unset($array[$key]);
        $array[$parent] = $this->parse_multi_depth_array($array[$parent]);
      }
    }
    return $array;
  }
  
  /**
   * Handle an IPC event. Called only upon SIGUSR2.
   */
  
  function _ipc_event()
  {
    /*
    case 'set_addr':
      $this->bind_address = $param;
      break;
    case 'set_port':
      $this->port = intval($param);
      break;
    case 'set_fork':
      $this->allow_fork = ( $param == '1' );
      break;
    case 'reboot':
      if ( !$this->threader->is_child() )
      {
        list(, $addr, $port, $fork) = explode(' ', $line);
        $fork = ( $fork === '1' );
        $this->reboot($addr, intval($port), $fork);
      }
      break;
        default:
          if ( isset($this->ipc_handlers[$action]) )
          {
            @call_user_func($this->ipc_handlers[$action], $line);
          }
          break;
      }
      */
  }
}

/**
 * Socket abstraction layer - low-level socket functions (socket_*)
 */

class Socket_Raw
{
  var $sock = array();
  var $socket_initted = false;
  
  function tcp_listen($address, $port)
  {
    // do we have socket functions?
    if ( !function_exists('socket_create') )
    {
      burnout('System does not support socket functions. Please rebuild your PHP or install an appropriate extension.');
    }
    
    $sockid = count($this->sock);
    
    $socktype = ( strstr($address, ':') ) ? AF_INET6 : AF_INET;
    $this->sock[$sockid] = @socket_create($socktype, SOCK_STREAM, getprotobyname('tcp'));
    if ( !$this->sock[$sockid] )
      throw new Exception('Could not create socket');
    $result = @socket_bind($this->sock[$sockid], $address, $port);
    if ( !$result )
      throw new Exception("Could not bind to $address:$port");
    $this->socket_initted = true;
    $result = @socket_listen($this->sock[$sockid], SOMAXCONN);
    if ( !$result )
      throw new Exception("Could not listen for connections $address:$port");
    
    $this->socket_initted = true;
  }
  
  function destroy()
  {
    if ( $this->socket_initted )
    {
      // http://us3.php.net/manual/en/function.socket-bind.php
      if ( is_array($this->sock) )
      {
        foreach ( $this->sock as $sock )
        {
          if ( !@socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1) )
          {
            echo socket_strerror(socket_last_error($sock)) . "\n";
          }
          @socket_shutdown($sock, 2);
          @socket_close($sock);
        }
      }
      else
      {
        if ( !@socket_set_option($this->sock, SOL_SOCKET, SO_REUSEADDR, 1) )
        {
          echo socket_strerror(socket_last_error($this->sock)) . "\n";
        }
        @socket_shutdown($this->sock, 2);
        @socket_close($this->sock);
      }
    }
  }
  
  function accept()
  {
    $remote = false;
    foreach ( $this->sock as $sock )
    {
      $timeout = 200000;
      switch(@socket_select($r = array($sock), $w = array($sock), $e = array($sock), 0, $timeout)) {
        case 2:
          return false;
        case 1:
          $remote = @socket_accept($sock);
          $return = new Socket_Raw();
          $return->sock = $remote;
          $return->socket_initted = true;
          return $return;
          break;
        case 0:
          continue;
      }
    }
  }
  
  /**
   * Closes the socket but doesn't destroy it.
   */
  
  function soft_shutdown()
  {
    @socket_set_option($this->sock, SOL_SOCKET, SO_REUSEADDR, 1);
    socket_close($this->sock);
  }
  
  function set_timeout($timeout, $usec = false)
  {
    // doesn't work in this.
  }
  
  function read_normal($length = 1024)
  {
    return @socket_read($this->sock, $length, PHP_NORMAL_READ);
  }
  
  function read_binary($length = 1024)
  {
    return @socket_read($this->sock, $length, PHP_BINARY_READ);
  }
  
  function timed_out()
  {
    $md = @socket_get_status($this->sock);
    return ( isset($md['timed_out']) ) ? $md['timed_out'] : false;
  }
  
  function get_peer_info(&$addr, &$port)
  {
    socket_getpeername($this->sock, $addr, $port);
  }
  
  function write($data)
  {
    return @socket_write($this->sock, $data);
  }
  
  function is_eof()
  {
    // feof() not supported
    return false;
  }
}

/**
 * Socket abstraction layer - PHP stream support
 */

class Socket_Stream
{
  var $sock = array();
  var $socket_initted = false;
  
  function tcp_listen($address, $port)
  {
    // does PHP support this?
    if ( !function_exists('stream_socket_server') )
    {
      burnout('System does not support stream functions. Please rebuild your PHP or install an appropriate extension.');
    }
    
    if ( strstr($address, ':') )
    {
      // ipv6 address (probably)
      $address = "[$address]";
    }
    
    $sockid = count($this->sock);
    
    $this->sock[$sockid] = @stream_socket_server("tcp://$address:$port", $errno, $errstr);
    if ( !$this->sock[$sockid] )
    {
      throw new Exception("Could not create the socket: error $errno: $errstr");
    }
  }
  
  function destroy()
  {
    if ( $this->socket_initted )
    {
      // PHP >= 5.2.1
      if ( is_array($this->sock) )
      {
        foreach ( $this->sock as $sock )
        {
          if ( function_exists('stream_socket_shutdown') )
          {
            @stream_socket_shutdown($sock, STREAM_SHUT_RDWR);
          }
          while ( !@fclose($sock) )
          {
            usleep(100000);
          }
        }
      }
      else
      {
        if ( function_exists('stream_socket_shutdown') )
        {
          @stream_socket_shutdown($this->sock, STREAM_SHUT_RDWR);
        }
        while ( !@fclose($this->sock) )
        {
          usleep(100000);
        }
      }
    }
  }
  
  function accept()
  {
    // the goal of a custom accept() with *_select() is to tick every 200ms to allow signals.
    foreach ( $this->sock as $sock )
      stream_set_blocking($sock, 1);
    $timeout = 200000;
    $r = $this->sock;
    $selection = @stream_select($r, $w = array($sock), $e = array($sock), 0, $timeout);
    if ( !$selection )
    {
      return false;
    }
    $remote = stream_socket_accept($r[0]);
    $return = new Socket_Stream();
    $return->sock = $remote;
    $return->socket_initted = true;
    return $return;
  }
  
  function soft_shutdown()
  {
    fclose($this->sock);
  }
  
  function set_timeout($timeout, $usec = false)
  {
    return ( $usec ) ? @stream_set_timeout($this->sock, 0, $usec) : @stream_set_timeout($this->sock, $timeout);
  }
  
  function read_normal($length = 1024)
  {
    return @fgets($this->sock, $length);
  }
  
  function read_binary($length = 1024)
  {
    return @fread($this->sock, $length);
  }
  
  function timed_out()
  {
    $md = @stream_get_meta_data($this->sock);
    return ( isset($md['timed_out']) ) ? $md['timed_out'] : false;
  }
  
  function get_peer_info(&$addr, &$port)
  {
    $peer = stream_socket_get_name($this->sock, true);
    $addr = substr($peer, 0, strrpos($peer, ':'));
    $port = substr($peer, strrpos($peer, ':') + 1);
  }
  
  function write($data)
  {
    $size = strlen($data);
    $written = 0;
    while ( $written < $size )
    {
      $written += @fwrite($this->sock, substr($data, $written));
    }
    return true;
  }
  
  function is_eof()
  {
    return feof($this->sock);
  }
}

/**
 * Exception class that allows breaking directly out of a scriptlet.
 */

class HttpExceptionFatal extends Exception
{
}

/**
 * Exception class that will be treated as a scriptlet ending with success.
 */

class HttpSuccess extends Exception
{
}

/**
 * Array of known HTTP status/error codes
 */
 
$http_responses = array(
    200 => 'OK',
    302 => 'Found',
    307 => 'Temporary Redirect',
    400 => 'Bad Request',
    401 => 'Unauthorized',
    403 => 'Forbidden',
    404 => 'Not Found',
    405 => 'Method Not Allowed',
    406 => 'Not Acceptable',
    500 => 'Internal Server Error',
    501 => 'Not Implemented'
  );

/**
 * Array of default extension->mimetype mappings
 */

$mime_types = array(
    'html' => 'text/html',
    'htm'  => 'text/html',
    'png'  => 'image/png',
    'gif'  => 'image/gif',
    'jpeg' => 'image/jpeg',
    'jpg'  => 'image/jpeg',
    'js'   => 'text/javascript',
    'json' => 'text/x-javascript-json',
    'css'  => 'text/css',
    'php'  => 'application/x-httpd-php'
  );