webserver.php
changeset 0 c63de9eb7045
child 5 9b96265b5918
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/webserver.php	Sun Mar 23 14:59:33 2008 -0400
@@ -0,0 +1,638 @@
+<?php
+
+/**
+ * Webserver class
+ * 
+ * Web control interface script for Amarok
+ * Written by Dan Fuhry - 2008
+ *
+ * This script is in the public domain. Use it for good, not evil.
+ */
+
+/**
+ * Version of the server
+ * @const string
+ */
+
+define('HTTPD_VERSION', '0.1b1');
+
+/**
+ * Simple web server written in PHP.
+ * @package Amarok
+ * @subpackage WebControl
+ * @author Dan Fuhry
+ * @license Public domain
+ */
+
+class WebServer
+{
+  
+  /**
+   * IP address we're bound to
+   * @var string
+   */
+  
+  var $bind_address = '127.0.0.1';
+  
+  /**
+   * Socket resource
+   * @var resource
+   */
+  
+  var $sock = null;
+  
+  /**
+   * Server string
+   * @var string
+   */
+  
+  var $server_string = 'PhpHttpd';
+  
+  /**
+   * Default document (well default handler)
+   * @var string
+   */
+  
+  var $default_document = false;
+  
+  /**
+   * 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;
+  
+  /**
+   * Constructor.
+   * @param string IPv4 address to bind to
+   * @param int Port number
+   */
+  
+  function __construct($address = '127.0.0.1', $port = 8080)
+  {
+    @set_time_limit(0);
+    @ini_set('memory_limit', '256M');
+    
+    $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");
+    $result = socket_listen($this->sock, SOMAXCONN);
+    if ( !$result )
+      throw new Exception("Could not listen for connections $address:$port");
+    $this->bind_address = $address;
+    $this->server_string = "PhpHttpd/" . HTTPD_VERSION . " PHP/" . PHP_VERSION . "\r\n";
+  }
+  
+  /**
+   * Destructor.
+   */
+  
+  function __destruct()
+  {
+    status('WebServer: destroying socket');
+    @socket_close($this->sock);
+  }
+  
+  /**
+   * Main server loop
+   */
+  
+  function serve()
+  {
+    while ( true )
+    {
+      // wait for connection...
+      $remote = socket_accept($this->sock);
+      // read request
+      $last_line = '';
+      $client_headers = '';
+      while ( $line = socket_read($remote, 1024, PHP_NORMAL_READ) )
+      {
+        $line = str_replace("\r", "", $line);
+        if ( empty($line) )
+          continue;
+        if ( $line == "\n" && $last_line == "\n" )
+          break;
+        $client_headers .= $line;
+        $last_line = $line;
+      }
+      
+      // parse request
+      $client_headers = trim($client_headers);
+      $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 headers
+      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];
+      }
+      
+      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);
+      }
+      
+      $postdata = '';
+      $_POST = array();
+      if ( $method == 'POST' )
+      {
+        // read POST data
+        if ( isset($_SERVER['HTTP_CONTENT_LENGTH']) )
+        {
+          $postdata = socket_read($remote, intval($_SERVER['HTTP_CONTENT_LENGTH']), PHP_BINARY_READ);
+        }
+        else
+        {
+          $postdata = socket_read($remote, 8388608, PHP_NORMAL_READ);
+        }
+        if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]-]+)(=[^ &]+)?/', $postdata, $matches) )
+        {
+          if ( isset($matches[1]) )
+          {
+            foreach ( $matches[0] as $i => $_ )
+            {
+              $_POST[$matches[2][$i]] = ( !empty($matches[3][$i]) ) ? urldecode(substr($matches[3][$i], 1)) : true;
+            }
+          }
+        }
+      }
+      
+      // parse URI
+      $params = '';
+      if ( strstr($uri, '?') )
+      {
+        $params = substr(strstr($uri, '?'), 1);
+        $uri    = substr($uri, 0, strpos($uri, '?'));
+      }
+      
+      $_SERVER['REQUEST_URI'] = '/' . rawurldecode($uri);
+      $_SERVER['REQUEST_METHOD'] = $method;
+      socket_getpeername($remote, $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT']);
+      
+      $_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;
+          }
+        }
+      }
+      
+      if ( $uri == '' )
+      {
+        $uri = strval($this->default_document);
+      }
+      
+      $uri_parts = explode('/', $uri);
+      
+      // loop through URI parts, see if a handler is set
+      $handler = false;
+      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 )
+      {
+        $this->send_http_error($remote, 404, "The requested URL /$uri was not found on this server.");
+        continue;
+      }
+      
+      $this->send_standard_response($remote, $handler, $uri, $params);
+      
+      @socket_close($remote);
+    }
+  }
+  
+  /**
+   * 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;
+    $reason_code = ( isset($http_responses[$http_code]) ) ? $http_responses[$http_code] : 'Unknown';
+    
+    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);
+    
+    socket_write($socket, "HTTP/1.1 $http_code $reason_code\r\n");
+    socket_write($socket, "Server: $this->server_string");
+    socket_write($socket, "Connection: close\r\n");
+    socket_write($socket, "Content-Type: $contenttype\r\n");
+    if ( !empty($headers) )
+    {
+      socket_write($socket, "$headers\r\n");
+    }
+    socket_write($socket, "\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 'dir':
+        // 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 = substr($uri, strlen($handler['id']) + 1);
+        
+        // get file path
+        $file_path = rtrim($handler['dir'], '/') . $uri;
+        if ( file_exists($file_path) )
+        {
+          // found it :-D
+          
+          // is this a directory?
+          if ( is_dir($file_path) )
+          {
+            if ( !$this->allow_dir_list )
+            {
+              $this->send_http_error($socket, 403, "Directory listing is not allowed.");
+              return true;
+            }
+            // yes, list contents
+            $root = '/' . $handler['id'] . rtrim($uri, '/');
+            $parent = substr($root, 0, strrpos($root, '/')) . '/';
+              
+            $contents = <<<EOF
+<html>
+  <head>
+    <title>Index of: $root</title>
+  </head>
+  <body>
+    <h1>Index of $root</h1>
+    <ul>
+      <li><a href="$parent">Parent directory</a></li>
+    
+EOF;
+            $dirs = array();
+            $files = array();
+            $d = @opendir($file_path);
+            while ( $dh = readdir($d) )
+            {
+              if ( $dh == '.' || $dh == '..' )
+                continue;
+              if ( is_dir("$file_path/$dh") )
+                $dirs[] = $dh;
+              else
+                $files[] = $dh;
+            }
+            asort($dirs);
+            asort($files);
+            foreach ( $dirs as $dh )
+            {
+              $contents .= '  <li><a href="' . $root . '/' . $dh . '">' . $dh . '/</a></li>' . "\n    ";
+            }
+            foreach ( $files as $dh )
+            {
+              $contents .= '  <li><a href="' . $root . '/' . $dh . '">' . $dh . '</a></li>' . "\n    ";
+            }
+            $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($socket, $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($socket, $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($socket, $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 'function':
+        // 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();
+          $result = @call_user_func($handler['function'], $this);
+          $output = ob_get_contents();
+          ob_end_clean();
+        }
+        catch ( Exception $e )
+        {
+          restore_error_handler();
+          $this->send_http_error($socket, 500, "A handler crashed with an exception; see the command line for details.");
+          status("caught exception in handler {$handler['id']}:\n$e");
+          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;
+        }
+        
+        $headers = implode("\r\n", $this->response_headers);
+        
+        // write headers
+        $this->send_client_headers($socket, $this->response_code, $this->content_type, $headers);
+        
+        // write body
+        socket_write($socket, $output);
+        
+        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;
+  }
+  
+  /**
+   * 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';
+    $this->send_client_headers($socket, $http_code);
+    $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;
+    socket_write($socket, $html);
+    @socket_close($socket);
+  }
+  
+  /**
+   * 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)
+  {
+    switch($type)
+    {
+      case 'dir':
+        $this->handlers[$uri] = array(
+            'type' => 'dir',
+            'dir' => $value
+          );
+        break;
+      case 'file':
+        $this->handlers[$uri] = array(
+            'type' => 'file',
+            'file' => $value
+          );
+        break;
+      case 'function':
+        $this->handlers[$uri] = array(
+            'type' => 'function',
+            '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>';
+  }
+  
+}
+
+/**
+ * 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 mime type->html 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'
+  );
+