author | Dan |
Fri, 25 Apr 2008 12:42:26 -0400 | |
changeset 18 | 69af47034212 |
parent 17 | 66c3eb4cdc4c |
child 21 | 74edc873234f |
permissions | -rw-r--r-- |
<?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. */ /** * 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; /** * Switch to control forking support. * @var bool */ var $allow_fork = true; /** * 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; /** * 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'); // 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"); $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() { if ( !defined('HTTPD_WS_CHILD') ) { status('WebServer: destroying socket'); @socket_shutdown($this->sock, 2); @socket_close($this->sock); } } /** * Main server loop */ function serve() { while ( true ) { // if this is a child process, we're finished - close up shop if ( defined('HTTPD_WS_CHILD') && !$this->in_keepalive ) { status('Exiting child process'); @socket_shutdown($remote); @socket_close($remote); exit(0); } // wait for connection... // trick from http://us.php.net/manual/en/function.socket-accept.php if ( !defined('HTTPD_WS_CHILD') ) { $remote = false; $timeout = 5; switch(@socket_select($r = array($this->sock), $w = array($this->sock), $e = array($this->sock), $timeout)) { case 2: break; case 1: $remote = @socket_accept($this->sock); break; case 0: break; } } if ( !$remote ) { $this->in_keepalive = false; continue; } // fork off if possible if ( function_exists('pcntl_fork') && $this->allow_fork && !$this->in_keepalive ) { $pid = pcntl_fork(); if ( $pid == -1 ) { // do nothing; continue responding to request in single-threaded mode } else if ( $pid ) { // we are the parent, continue listening socket_close($remote); continue; } else { // this is the child define('HTTPD_WS_CHILD', 1); socket_close($this->sock); } } $this->in_keepalive = false; // 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]; } // enable keep-alive if requested if ( isset($_SERVER['HTTP_CONNECTION']) && defined('HTTPD_WS_CHILD') ) { $this->in_keepalive = ( strtolower($_SERVER['HTTP_CONNECTION']) === 'keep-alive' ); } 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); if ( !$this->in_keepalive && defined('HTTPD_WS_CHILD') ) { // if ( defined('HTTPD_WS_CHILD') ) // status('Closing connection'); @socket_shutdown($remote); @socket_close($remote); status('Exiting child process'); exit(0); } else if ( defined('HTTPD_WS_CHILD') ) { // if ( defined('HTTPD_WS_CHILD') ) // status('Continuing connection'); // @socket_write($remote, "\r\n\r\n"); } } } /** * 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'; $_SERVER['HTTP_USER_AGENT'] = ( isset($_SERVER['HTTP_USER_AGENT']) ) ? $_SERVER['HTTP_USER_AGENT'] : '(no user agent)'; status("{$_SERVER['REMOTE_ADDR']} {$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']} $http_code {$_SERVER['HTTP_USER_AGENT']}"); $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($socket, "HTTP/1.1 $http_code $reason_code\r\n"); @socket_write($socket, "Server: $this->server_string"); @socket_write($socket, "Connection: $connection\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; } // $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($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' );