Should be nearly finished now - includes volume control, length measurement, and seems pretty stable
<?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'
);