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