webserver.php
author Dan
Tue, 23 Sep 2008 23:26:18 -0400
changeset 50 1b4288399b1f
parent 48 d643bfb862d8
child 53 a6b339665650
permissions -rw-r--r--
Added graphical configuration, at this point only for the grey theme but others will follow soon. (This has been nearly done for two weeks or more but was on hold due to the bugs with multithreading)

<?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['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;
    
    $class = 'Socket_' . HTTPD_SOCKET_LAYER;
    $this->server = new $class();
    $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();
    
    // 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->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 )
    {
      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
    {
      // this is a childless parent; delay any action until the current
      // request has been sent (do nothing now)
    }
  }
  
  /**
   * 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 )
      {
        if ( $this->threader->fork() == FORK_CHILD )
        {
          // this is the child
          define('HTTPD_WS_CHILD', 1);
        }
        else
        {
          // we are the parent, continue listening
          $remote->soft_shutdown();
          $this->child_list[] = $pid;
          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 process 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]);
      }
      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
          $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=" . date('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);
  }
  
  /**
   * 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;
  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.');
    }
    
    $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);
    if ( !$result )
      throw new Exception("Could not bind to $address:$port");
    $this->socket_initted = true;
    $result = @socket_listen($this->sock, 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 ( !@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;
    $timeout = 5;
    switch(@socket_select($r = array($this->sock), $w = array($this->sock), $e = array($this->sock), $timeout)) {
      case 2:
        return false;
      case 1:
        $remote = @socket_accept($this->sock);
        $return = new Socket_Raw();
        $return->sock = $remote;
        $return->socket_initted = true;
        return $return;
        break;
      case 0:
        return false;
    }
  }
  
  /**
   * 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;
  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.');
    }
    
    $this->sock = @stream_socket_server("tcp://$address:$port", $errno, $errstr);
    if ( !$this->sock )
      throw new Exception("Could not create the socket: error $errno: $errstr");
  }
  
  function destroy()
  {
    if ( $this->socket_initted )
    {
      // PHP >= 5.2.1
      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.
    stream_set_blocking($this->sock, 1);
    $timeout = 5;
    $selection = @stream_select($r = array($this->sock), $w = array($this->sock), $e = array($this->sock), $timeout);
    if ( !$selection )
    {
      return false;
    }
    $remote = stream_socket_accept($this->sock);
    $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);
    list($addr, $port) = explode(':', $peer);
  }
  
  function write($data)
  {
    return @fwrite($this->sock, $data);
  }
  
  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'
  );