webserver.php
changeset 0 c63de9eb7045
child 5 9b96265b5918
equal deleted inserted replaced
-1:000000000000 0:c63de9eb7045
       
     1 <?php
       
     2 
       
     3 /**
       
     4  * Webserver class
       
     5  * 
       
     6  * Web control interface script for Amarok
       
     7  * Written by Dan Fuhry - 2008
       
     8  *
       
     9  * This script is in the public domain. Use it for good, not evil.
       
    10  */
       
    11 
       
    12 /**
       
    13  * Version of the server
       
    14  * @const string
       
    15  */
       
    16 
       
    17 define('HTTPD_VERSION', '0.1b1');
       
    18 
       
    19 /**
       
    20  * Simple web server written in PHP.
       
    21  * @package Amarok
       
    22  * @subpackage WebControl
       
    23  * @author Dan Fuhry
       
    24  * @license Public domain
       
    25  */
       
    26 
       
    27 class WebServer
       
    28 {
       
    29   
       
    30   /**
       
    31    * IP address we're bound to
       
    32    * @var string
       
    33    */
       
    34   
       
    35   var $bind_address = '127.0.0.1';
       
    36   
       
    37   /**
       
    38    * Socket resource
       
    39    * @var resource
       
    40    */
       
    41   
       
    42   var $sock = null;
       
    43   
       
    44   /**
       
    45    * Server string
       
    46    * @var string
       
    47    */
       
    48   
       
    49   var $server_string = 'PhpHttpd';
       
    50   
       
    51   /**
       
    52    * Default document (well default handler)
       
    53    * @var string
       
    54    */
       
    55   
       
    56   var $default_document = false;
       
    57   
       
    58   /**
       
    59    * HTTP response code set by the handler function
       
    60    * @var int
       
    61    */
       
    62   
       
    63   var $response_code = 0;
       
    64   
       
    65   /**
       
    66    * Content type set by the current handler function
       
    67    * @var string
       
    68    */
       
    69   
       
    70   var $content_type = '';
       
    71   
       
    72   /**
       
    73    * Response headers to send back to the client
       
    74    * @var array
       
    75    */
       
    76   
       
    77   var $response_headers = array();
       
    78   
       
    79   /**
       
    80    * List of handlers
       
    81    * @var array
       
    82    */
       
    83    
       
    84   var $handlers = array();
       
    85   
       
    86   /**
       
    87    * Switch to control if directory listing is enabled
       
    88    * @var bool
       
    89    */
       
    90   
       
    91   var $allow_dir_list = false;
       
    92   
       
    93   /**
       
    94    * Constructor.
       
    95    * @param string IPv4 address to bind to
       
    96    * @param int Port number
       
    97    */
       
    98   
       
    99   function __construct($address = '127.0.0.1', $port = 8080)
       
   100   {
       
   101     @set_time_limit(0);
       
   102     @ini_set('memory_limit', '256M');
       
   103     
       
   104     $this->sock = socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp'));
       
   105     if ( !$this->sock )
       
   106       throw new Exception('Could not create socket');
       
   107     $result = socket_bind($this->sock, $address, $port);
       
   108     if ( !$result )
       
   109       throw new Exception("Could not bind to $address:$port");
       
   110     $result = socket_listen($this->sock, SOMAXCONN);
       
   111     if ( !$result )
       
   112       throw new Exception("Could not listen for connections $address:$port");
       
   113     $this->bind_address = $address;
       
   114     $this->server_string = "PhpHttpd/" . HTTPD_VERSION . " PHP/" . PHP_VERSION . "\r\n";
       
   115   }
       
   116   
       
   117   /**
       
   118    * Destructor.
       
   119    */
       
   120   
       
   121   function __destruct()
       
   122   {
       
   123     status('WebServer: destroying socket');
       
   124     @socket_close($this->sock);
       
   125   }
       
   126   
       
   127   /**
       
   128    * Main server loop
       
   129    */
       
   130   
       
   131   function serve()
       
   132   {
       
   133     while ( true )
       
   134     {
       
   135       // wait for connection...
       
   136       $remote = socket_accept($this->sock);
       
   137       // read request
       
   138       $last_line = '';
       
   139       $client_headers = '';
       
   140       while ( $line = socket_read($remote, 1024, PHP_NORMAL_READ) )
       
   141       {
       
   142         $line = str_replace("\r", "", $line);
       
   143         if ( empty($line) )
       
   144           continue;
       
   145         if ( $line == "\n" && $last_line == "\n" )
       
   146           break;
       
   147         $client_headers .= $line;
       
   148         $last_line = $line;
       
   149       }
       
   150       
       
   151       // parse request
       
   152       $client_headers = trim($client_headers);
       
   153       $client_headers = explode("\n", $client_headers);
       
   154       
       
   155       // first line
       
   156       $request = $client_headers[0];
       
   157       if ( !preg_match('/^(GET|POST) \/([^ ]*) HTTP\/1\.[01]$/', $request, $match) )
       
   158       {
       
   159         $this->send_http_error($remote, 400, 'Your client issued a malformed or illegal request.');
       
   160         continue;
       
   161       }
       
   162       $method =& $match[1];
       
   163       $uri =& $match[2];
       
   164       
       
   165       // set client headers
       
   166       unset($client_headers[0]);
       
   167       foreach ( $client_headers as $line )
       
   168       {
       
   169         if ( !preg_match('/^([A-z0-9-]+): (.+)$/is', $line, $match) )
       
   170           continue;
       
   171         $key = 'HTTP_' . strtoupper(str_replace('-', '_', $match[1]));
       
   172         $_SERVER[$key] = $match[2];
       
   173       }
       
   174       
       
   175       if ( isset($_SERVER['HTTP_AUTHORIZATION']) )
       
   176       {
       
   177         $data = $_SERVER['HTTP_AUTHORIZATION'];
       
   178         $data = substr(strstr($data, ' '), 1);
       
   179         $data = base64_decode($data);
       
   180         $_SERVER['PHP_AUTH_USER'] = substr($data, 0, strpos($data, ':'));
       
   181         $_SERVER['PHP_AUTH_PW'] = substr(strstr($data, ':'), 1);
       
   182       }
       
   183       
       
   184       $postdata = '';
       
   185       $_POST = array();
       
   186       if ( $method == 'POST' )
       
   187       {
       
   188         // read POST data
       
   189         if ( isset($_SERVER['HTTP_CONTENT_LENGTH']) )
       
   190         {
       
   191           $postdata = socket_read($remote, intval($_SERVER['HTTP_CONTENT_LENGTH']), PHP_BINARY_READ);
       
   192         }
       
   193         else
       
   194         {
       
   195           $postdata = socket_read($remote, 8388608, PHP_NORMAL_READ);
       
   196         }
       
   197         if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]-]+)(=[^ &]+)?/', $postdata, $matches) )
       
   198         {
       
   199           if ( isset($matches[1]) )
       
   200           {
       
   201             foreach ( $matches[0] as $i => $_ )
       
   202             {
       
   203               $_POST[$matches[2][$i]] = ( !empty($matches[3][$i]) ) ? urldecode(substr($matches[3][$i], 1)) : true;
       
   204             }
       
   205           }
       
   206         }
       
   207       }
       
   208       
       
   209       // parse URI
       
   210       $params = '';
       
   211       if ( strstr($uri, '?') )
       
   212       {
       
   213         $params = substr(strstr($uri, '?'), 1);
       
   214         $uri    = substr($uri, 0, strpos($uri, '?'));
       
   215       }
       
   216       
       
   217       $_SERVER['REQUEST_URI'] = '/' . rawurldecode($uri);
       
   218       $_SERVER['REQUEST_METHOD'] = $method;
       
   219       socket_getpeername($remote, $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT']);
       
   220       
       
   221       $_GET = array();
       
   222       if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]-]+)(=[^ &]+)?/', $params, $matches) )
       
   223       {
       
   224         if ( isset($matches[1]) )
       
   225         {
       
   226           foreach ( $matches[0] as $i => $_ )
       
   227           {
       
   228             $_GET[$matches[2][$i]] = ( !empty($matches[3][$i]) ) ? urldecode(substr($matches[3][$i], 1)) : true;
       
   229           }
       
   230         }
       
   231       }
       
   232       
       
   233       if ( $uri == '' )
       
   234       {
       
   235         $uri = strval($this->default_document);
       
   236       }
       
   237       
       
   238       $uri_parts = explode('/', $uri);
       
   239       
       
   240       // loop through URI parts, see if a handler is set
       
   241       $handler = false;
       
   242       for ( $i = count($uri_parts) - 1; $i >= 0; $i-- )
       
   243       {
       
   244         $handler_test = implode('/', $uri_parts);
       
   245         if ( isset($this->handlers[$handler_test]) )
       
   246         {
       
   247           $handler = $this->handlers[$handler_test];
       
   248           $handler['id'] = $handler_test;
       
   249           break;
       
   250         }
       
   251         unset($uri_parts[$i]);
       
   252       }
       
   253       
       
   254       if ( !$handler )
       
   255       {
       
   256         $this->send_http_error($remote, 404, "The requested URL /$uri was not found on this server.");
       
   257         continue;
       
   258       }
       
   259       
       
   260       $this->send_standard_response($remote, $handler, $uri, $params);
       
   261       
       
   262       @socket_close($remote);
       
   263     }
       
   264   }
       
   265   
       
   266   /**
       
   267    * Sends the client appropriate response headers.
       
   268    * @param resource Socket connection to client
       
   269    * @param int HTTP status code, defaults to 200
       
   270    * @param string Content type, defaults to text/html
       
   271    * @param string Additional headers to send, optional
       
   272    */                     
       
   273   
       
   274   function send_client_headers($socket, $http_code = 200, $contenttype = 'text/html', $headers = '')
       
   275   {
       
   276     global $http_responses;
       
   277     $reason_code = ( isset($http_responses[$http_code]) ) ? $http_responses[$http_code] : 'Unknown';
       
   278     
       
   279     status("{$_SERVER['REMOTE_ADDR']} {$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']} $http_code {$_SERVER['HTTP_USER_AGENT']}");
       
   280     
       
   281     $headers = str_replace("\r\n", "\n", $headers);
       
   282     $headers = str_replace("\n", "\r\n", $headers);
       
   283     $headers = preg_replace("#[\r\n]+$#", '', $headers);
       
   284     
       
   285     socket_write($socket, "HTTP/1.1 $http_code $reason_code\r\n");
       
   286     socket_write($socket, "Server: $this->server_string");
       
   287     socket_write($socket, "Connection: close\r\n");
       
   288     socket_write($socket, "Content-Type: $contenttype\r\n");
       
   289     if ( !empty($headers) )
       
   290     {
       
   291       socket_write($socket, "$headers\r\n");
       
   292     }
       
   293     socket_write($socket, "\r\n");
       
   294   }
       
   295   
       
   296   /**
       
   297    * Sends a normal response
       
   298    * @param resource Socket connection to client
       
   299    * @param array Handler
       
   300    */
       
   301   
       
   302   function send_standard_response($socket, $handler)
       
   303   {
       
   304     switch ( $handler['type'] )
       
   305     {
       
   306       case 'dir':
       
   307         // security
       
   308         $uri = str_replace("\000", '', $_SERVER['REQUEST_URI']);
       
   309         if ( preg_match('#(\.\./|\/\.\.)#', $uri) || strstr($uri, "\r") || strstr($uri, "\n") )
       
   310         {
       
   311           $this->send_http_error($socket, 403, 'Access to this resource is forbidden.');
       
   312         }
       
   313         
       
   314         // import mimetypes
       
   315         global $mime_types;
       
   316         
       
   317         // trim handler id from uri
       
   318         $uri = substr($uri, strlen($handler['id']) + 1);
       
   319         
       
   320         // get file path
       
   321         $file_path = rtrim($handler['dir'], '/') . $uri;
       
   322         if ( file_exists($file_path) )
       
   323         {
       
   324           // found it :-D
       
   325           
       
   326           // is this a directory?
       
   327           if ( is_dir($file_path) )
       
   328           {
       
   329             if ( !$this->allow_dir_list )
       
   330             {
       
   331               $this->send_http_error($socket, 403, "Directory listing is not allowed.");
       
   332               return true;
       
   333             }
       
   334             // yes, list contents
       
   335             $root = '/' . $handler['id'] . rtrim($uri, '/');
       
   336             $parent = substr($root, 0, strrpos($root, '/')) . '/';
       
   337               
       
   338             $contents = <<<EOF
       
   339 <html>
       
   340   <head>
       
   341     <title>Index of: $root</title>
       
   342   </head>
       
   343   <body>
       
   344     <h1>Index of $root</h1>
       
   345     <ul>
       
   346       <li><a href="$parent">Parent directory</a></li>
       
   347     
       
   348 EOF;
       
   349             $dirs = array();
       
   350             $files = array();
       
   351             $d = @opendir($file_path);
       
   352             while ( $dh = readdir($d) )
       
   353             {
       
   354               if ( $dh == '.' || $dh == '..' )
       
   355                 continue;
       
   356               if ( is_dir("$file_path/$dh") )
       
   357                 $dirs[] = $dh;
       
   358               else
       
   359                 $files[] = $dh;
       
   360             }
       
   361             asort($dirs);
       
   362             asort($files);
       
   363             foreach ( $dirs as $dh )
       
   364             {
       
   365               $contents .= '  <li><a href="' . $root . '/' . $dh . '">' . $dh . '/</a></li>' . "\n    ";
       
   366             }
       
   367             foreach ( $files as $dh )
       
   368             {
       
   369               $contents .= '  <li><a href="' . $root . '/' . $dh . '">' . $dh . '</a></li>' . "\n    ";
       
   370             }
       
   371             $contents .= "\n    </ul>\n    <address>Served by {$this->server_string}</address>\n</body>\n</html>\n\n";
       
   372             
       
   373             $sz = strlen($contents);
       
   374             $this->send_client_headers($socket, 200, 'text/html', "Content-length: $sz\r\n");
       
   375             
       
   376             socket_write($socket, $contents);
       
   377             
       
   378             return true;
       
   379           }
       
   380           
       
   381           // try to open the file
       
   382           $fh = @fopen($file_path, 'r');
       
   383           if ( !$fh )
       
   384           {
       
   385             // can't open it, send a 404
       
   386             $this->send_http_error($socket, 404, "The requested URL " . htmlspecialchars($_SERVER['REQUEST_URI']) . " was not found on this server.");
       
   387           }
       
   388           
       
   389           // get size
       
   390           $sz = filesize($file_path);
       
   391           
       
   392           // mod time
       
   393           $time = date('r', filemtime($file_path));
       
   394           
       
   395           // all good, send headers
       
   396           $fileext = substr($file_path, strrpos($file_path, '.') + 1);
       
   397           $mimetype = ( isset($mime_types[$fileext]) ) ? $mime_types[$fileext] : 'application/octet-stream';
       
   398           $this->send_client_headers($socket, 200, $mimetype, "Content-length: $sz\r\nLast-Modified: $time\r\n");
       
   399           
       
   400           // send body
       
   401           while ( $blk = @fread($fh, 768000) )
       
   402           {
       
   403             socket_write($socket, $blk);
       
   404           }
       
   405           fclose($fh);
       
   406           return true;
       
   407         }
       
   408         else
       
   409         {
       
   410           $this->send_http_error($socket, 404, "The requested URL " . htmlspecialchars($_SERVER['REQUEST_URI']) . " was not found on this server.");
       
   411         }
       
   412         
       
   413         break;
       
   414       case 'file':
       
   415         
       
   416         // import mimetypes
       
   417         global $mime_types;
       
   418         
       
   419         // get file path
       
   420         $file_path = $handler['file'];
       
   421         if ( file_exists($file_path) )
       
   422         {
       
   423           // found it :-D
       
   424           
       
   425           // is this a directory?
       
   426           if ( is_dir($file_path) )
       
   427           {
       
   428             $this->send_http_error($socket, 500, "Host script mapped a directory as a file entry.");
       
   429             return true;
       
   430           }
       
   431           
       
   432           // try to open the file
       
   433           $fh = @fopen($file_path, 'r');
       
   434           if ( !$fh )
       
   435           {
       
   436             // can't open it, send a 404
       
   437             $this->send_http_error($socket, 404, "The requested URL " . htmlspecialchars($_SERVER['REQUEST_URI']) . " was not found on this server.");
       
   438           }
       
   439           
       
   440           // get size
       
   441           $sz = filesize($file_path);
       
   442           
       
   443           // mod time
       
   444           $time = date('r', filemtime($file_path));
       
   445           
       
   446           // all good, send headers
       
   447           $fileext = substr($file_path, strrpos($file_path, '.') + 1);
       
   448           $mimetype = ( isset($mime_types[$fileext]) ) ? $mime_types[$fileext] : 'application/octet-stream';
       
   449           $this->send_client_headers($socket, 200, $mimetype, "Content-length: $sz\r\nLast-Modified: $time\r\n");
       
   450           
       
   451           // send body
       
   452           while ( $blk = @fread($fh, 768000) )
       
   453           {
       
   454             socket_write($socket, $blk);
       
   455           }
       
   456           fclose($fh);
       
   457           return true;
       
   458         }
       
   459         else
       
   460         {
       
   461           $this->send_http_error($socket, 404, "The requested URL " . htmlspecialchars($_SERVER['REQUEST_URI']) . " was not found on this server.");
       
   462         }
       
   463         
       
   464         break;
       
   465       case 'function':
       
   466         // init vars
       
   467         $this->content_type = 'text/html';
       
   468         $this->response_code = 200;
       
   469         $this->response_headers = array();
       
   470         
       
   471         // error handling
       
   472         @set_error_handler(array($this, 'function_error_handler'), E_ALL);
       
   473         try
       
   474         {
       
   475           ob_start();
       
   476           $result = @call_user_func($handler['function'], $this);
       
   477           $output = ob_get_contents();
       
   478           ob_end_clean();
       
   479         }
       
   480         catch ( Exception $e )
       
   481         {
       
   482           restore_error_handler();
       
   483           $this->send_http_error($socket, 500, "A handler crashed with an exception; see the command line for details.");
       
   484           status("caught exception in handler {$handler['id']}:\n$e");
       
   485           return true;
       
   486         }
       
   487         restore_error_handler();
       
   488         
       
   489         // the handler function should return this magic string if it writes its own headers and socket data
       
   490         if ( $output == '__break__' )
       
   491         {
       
   492           return true;
       
   493         }
       
   494         
       
   495         $headers = implode("\r\n", $this->response_headers);
       
   496         
       
   497         // write headers
       
   498         $this->send_client_headers($socket, $this->response_code, $this->content_type, $headers);
       
   499         
       
   500         // write body
       
   501         socket_write($socket, $output);
       
   502         
       
   503         break;
       
   504     }
       
   505   }
       
   506   
       
   507   /**
       
   508    * Adds an HTTP header value to send back to the client
       
   509    * @var string Header
       
   510    */
       
   511   
       
   512   function header($str)
       
   513   {
       
   514     if ( preg_match('#HTTP/1\.[01] ([0-9]+) (.+?)[\s]*$#', $str, $match) )
       
   515     {
       
   516       $this->response_code = intval($match[1]);
       
   517       return true;
       
   518     }
       
   519     else if ( preg_match('#Content-type: ([^ ;]+)#i', $str, $match) )
       
   520     {
       
   521       $this->content_type = $match[1];
       
   522       return true;
       
   523     }
       
   524     $this->response_headers[] = $str;
       
   525     return true;
       
   526   }
       
   527   
       
   528   /**
       
   529    * Sends the client an HTTP error page
       
   530    * @param resource Socket connection to client
       
   531    * @param int HTTP status code
       
   532    * @param string Detailed error string
       
   533    */
       
   534   
       
   535   function send_http_error($socket, $http_code, $errstring)
       
   536   {
       
   537     global $http_responses;
       
   538     $reason_code = ( isset($http_responses[$http_code]) ) ? $http_responses[$http_code] : 'Unknown';
       
   539     $this->send_client_headers($socket, $http_code);
       
   540     $html = <<<EOF
       
   541 <html>
       
   542   <head>
       
   543     <title>$http_code $reason_code</title>
       
   544   </head>
       
   545   <body>
       
   546     <h1>$http_code $reason_code</h1>
       
   547     <p>$errstring</p>
       
   548     <hr />
       
   549     <address>Served by $this->server_string</address>
       
   550   </body>
       
   551 </html>
       
   552 EOF;
       
   553     socket_write($socket, $html);
       
   554     @socket_close($socket);
       
   555   }
       
   556   
       
   557   /**
       
   558    * Adds a new handler
       
   559    * @param string URI, minus the initial /
       
   560    * @param string Type of handler - function or dir
       
   561    * @param string Value - function name or absolute/relative path to directory
       
   562    */
       
   563   
       
   564   function add_handler($uri, $type, $value)
       
   565   {
       
   566     switch($type)
       
   567     {
       
   568       case 'dir':
       
   569         $this->handlers[$uri] = array(
       
   570             'type' => 'dir',
       
   571             'dir' => $value
       
   572           );
       
   573         break;
       
   574       case 'file':
       
   575         $this->handlers[$uri] = array(
       
   576             'type' => 'file',
       
   577             'file' => $value
       
   578           );
       
   579         break;
       
   580       case 'function':
       
   581         $this->handlers[$uri] = array(
       
   582             'type' => 'function',
       
   583             'function' => $value
       
   584           );
       
   585         break;
       
   586     }
       
   587   }
       
   588   
       
   589   /**
       
   590    * Error handling function
       
   591    * @param see <http://us.php.net/manual/en/function.set-error-handler.php>
       
   592    */
       
   593   
       
   594   function function_error_handler($errno, $errstr, $errfile, $errline, $errcontext)
       
   595   {
       
   596     echo '<div style="border: 1px solid #AA0000; background-color: #FFF0F0; padding: 10px;">';
       
   597     echo "<b>PHP warning/error:</b> type $errno ($errstr) caught in <b>$errfile</b> on <b>$errline</b><br />";
       
   598     echo "Error context:<pre>" . htmlspecialchars(print_r($errcontext, true)) . "</pre>";
       
   599     echo '</div>';
       
   600   }
       
   601   
       
   602 }
       
   603 
       
   604 /**
       
   605  * Array of known HTTP status/error codes
       
   606  */
       
   607  
       
   608 $http_responses = array(
       
   609     200 => 'OK',
       
   610     302 => 'Found',
       
   611     307 => 'Temporary Redirect',
       
   612     400 => 'Bad Request',
       
   613     401 => 'Unauthorized',
       
   614     403 => 'Forbidden',
       
   615     404 => 'Not Found',
       
   616     405 => 'Method Not Allowed',
       
   617     406 => 'Not Acceptable',
       
   618     500 => 'Internal Server Error',
       
   619     501 => 'Not Implemented'
       
   620   );
       
   621 
       
   622 /**
       
   623  * Array of default mime type->html mappings
       
   624  */
       
   625 
       
   626 $mime_types = array(
       
   627     'html' => 'text/html',
       
   628     'htm'  => 'text/html',
       
   629     'png'  => 'image/png',
       
   630     'gif'  => 'image/gif',
       
   631     'jpeg' => 'image/jpeg',
       
   632     'jpg'  => 'image/jpeg',
       
   633     'js'   => 'text/javascript',
       
   634     'json' => 'text/x-javascript-json',
       
   635     'css'  => 'text/css',
       
   636     'php'  => 'application/x-httpd-php'
       
   637   );
       
   638