<?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.1b4');
/**
* Length of keep-alive connections
* @const int
*/
define('HTTPD_KEEP_ALIVE_TIMEOUT', 300);
/**
* Webserver system icons
*/
define('HTTPD_ICON_SCRIPT', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGSSURBVCjPVVFNSwJhEF78Ad79Cf6PvXQRsotUlzKICosuRYmR2RJR0KE6lBFFZVEbpFBSqKu2rum6llFS9HHI4iUhT153n6ZtIWMOM+/MM88z7wwH7s9Ub16SJcnbmrNcxVm2q7Z8/QPvEOtntpj92NkCqITLepEpjix7xQtiLOoQ2b6+E7YAN/5nfOEJ2WbKqOIOJ4bYVMEQx4LfBBQDsvFMhUcCVU1/CxVXmDBGA5ZETrhDCQVcYAPbyEJBhvrnBVPiSpNr6cYDNCQwo4zzU/ySckkgDYuNuVpI42T9k4gLKGMPs/xPzzovQiY2hQYe0jlJfyNNhTqiWDYBq/wBMcSRpnyPzu1oS7WtxjVBSthU1vgVksiQ3Dn6Gp5ah2YOKQo5GiuHPA6xT1EKpxQNCNYejgIR457KKio0S56YckjSa9jo//3mrj+BV0QQagqGTOo+Y7gZIf1puP3WHoLhEb2PjTlCTCWGXtbp8DCX3hZuOdaIc9A+aQvWk4ihq95p67a7nP+u+Ws+r0dql9z/zv0NCYhdCPKZ7oYAAAAASUVORK5CYII=');
define('HTTPD_ICON_FOLDER', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGrSURBVDjLxZO7ihRBFIa/6u0ZW7GHBUV0UQQTZzd3QdhMQxOfwMRXEANBMNQX0MzAzFAwEzHwARbNFDdwEd31Mj3X7a6uOr9BtzNjYjKBJ6nicP7v3KqcJFaxhBVtZUAK8OHlld2st7Xl3DJPVONP+zEUV4HqL5UDYHr5xvuQAjgl/Qs7TzvOOVAjxjlC+ePSwe6DfbVegLVuT4r14eTr6zvA8xSAoBLzx6pvj4l+DZIezuVkG9fY2H7YRQIMZIBwycmzH1/s3F8AapfIPNF3kQk7+kw9PWBy+IZOdg5Ug3mkAATy/t0usovzGeCUWTjCz0B+Sj0ekfdvkZ3abBv+U4GaCtJ1iEm6ANQJ6fEzrG/engcKw/wXQvEKxSEKQxRGKE7Izt+DSiwBJMUSm71rguMYhQKrBygOIRStf4TiFFRBvbRGKiQLWP29yRSHKBTtfdBmHs0BUpgvtgF4yRFR+NUKi0XZcYjCeCG2smkzLAHkbRBmP0/Uk26O5YnUActBp1GsAI+S5nRJJJal5K1aAMrq0d6Tm9uI6zjyf75dAe6tx/SsWeD//o2/Ab6IH3/h25pOAAAAAElFTkSuQmCC');
define('HTTPD_ICON_FILE', 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAINSURBVBgZBcG/r55zGAfg6/4+z3va01NHlYgzEfE7MdCIGISFgS4Gk8ViYyM2Mdlsko4GSf8Do0FLRCIkghhYJA3aVBtEz3nP89wf11VJvPDepdd390+8Nso5nESBQoq0pfvXm9fzWf19453LF85vASqJlz748vInb517dIw6EyYBIIG49u+xi9/c9MdvR//99MPPZ7+4cP4IZhhTPbwzT2d+vGoaVRRp1rRliVvHq+cfvM3TD82+7mun0o/ceO7NT+/4/KOXjwZU1ekk0840bAZzMQ2mooqh0A72d5x/6sB9D5zYnff3PoYBoWBgFKPKqDKqjCpjKr//dcu9p489dra88cydps30KswACfNEKanSaxhlntjJ8Mv12Paie+vZ+0+oeSwwQ0Iw1xAR1CiFNJkGO4wu3ZMY1AAzBI0qSgmCNJsJUEOtJSMaCTBDLyQ0CknAGOgyTyFFiLI2awMzdEcSQgSAAKVUmAeNkxvWJWCGtVlDmgYQ0GFtgg4pNtOwbBcwQy/Rife/2yrRRVI0qYCEBly8Z+P4qMEMy7JaVw72N568e+iwhrXoECQkfH91kY7jwwXMsBx1L93ZruqrK6uuiAIdSnTIKKPLPFcvay8ww/Hh+ufeznTXu49v95IMoQG3784gYXdTqvRmqn/Wpa/ADFX58MW3L71SVU9ETgEIQQQIOOzub+fhIvwPRDgeVjWDahIAAAAASUVORK5CYII=');
/**
* Simple but full-featured embedded web server written in PHP.
* @package Amarok
* @subpackage WebControl
* @author Dan Fuhry
* @license GNU General Public License <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
*/
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;
/**
* List of filenames or handlers used when a directory listing is requested
* @var array
*/
var $directory_index = array('index.html', 'index.htm', 'index', 'default.html', 'default.htm');
/**
* 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;
/**
* UUID for this server instance
* @var string
*/
var $uuid = '00000000-0000-0000-0000-000000000000';
/**
* Switch to track whether a scriptlet is running. If it is, send_http_error() does more than normal.
* @var bool
*/
var $in_scriptlet = false;
/**
* Switch to track whether headers have been sent or not.
* @var bool
*/
var $headers_sent = false;
/**
* Switch to track if the socket is bound and thus needs to be freed or not
* @var bool
*/
var $socket_initted = false;
/**
* The list of child processes spawned by this server.
* @var array
*/
var $child_list = array();
/**
* The parent process's PID
* @var int
*/
var $parent_pid = 0;
/**
* Constructor.
* @param string IPv4 address to bind to
* @param int Port number
* @param int If port is under 1024, specify a user ID/name to switch to here
* @param int If port is under 1024, specify a group ID/name to switch to here
*/
function __construct($address = '127.0.0.1', $port = 8080, $targetuser = null, $targetgroup = null)
{
@set_time_limit(0);
@ini_set('memory_limit', '128M');
// 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.');
}
// make sure we're not running as root
// note that if allow_root is true, you must specify a UID/GID (or user/group) to switch to once the socket is bound
$allow_root = ( $port < 1024 ) ? true : false;
if ( function_exists('posix_geteuid') )
{
$euid = posix_geteuid();
$egid = posix_getegid();
$username = posix_getpwuid($euid);
$username = $username['name'];
$group = posix_getgrgid($egid);
$group = $group['name'];
if ( $euid == 0 && !$allow_root )
{
// running as root but not on a privileged port - die for security
burnout("Running as superuser (user \"$username\" and group \"$group\"). This is not allowed for security reasons.");
}
else if ( $euid == 0 && $allow_root )
{
// running as root and port below 1024, so notify of the switch and verify that a target UID and GID were passed
if ( $targetuser === null || $targetgroup === null )
{
// no target user/group specified
burnout("Must specify a target user and group when running server as root");
}
// get info about target user/group
if ( is_string($targetuser) )
{
$targetuser = posix_getpwnam($targetuser);
$targetuser = $targetuser['uid'];
}
if ( is_string($targetgroup) )
{
$targetgroup = posix_getgrnam($targetgroup);
$targetgroup = $targetgroup['uid'];
}
// make sure all info is valid
if ( !is_int($targetuser) || !is_int($targetgroup) )
{
burnout('Invalid user or group specified');
}
$userinfo = posix_getpwuid($targetuser);
$groupinfo = posix_getgrgid($targetgroup);
if ( function_exists('status') )
status("Will switch to user \"{$userinfo['name']}\" and group \"{$groupinfo['name']}\" shortly after binding to socket");
}
else if ( $allow_root && $euid > 0 )
{
burnout("Must be superuser to bind to ports below 1024");
}
}
$socket_do_root = ( $allow_root ) ? function_exists('posix_geteuid') : false;
$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");
$this->socket_initted = true;
$result = @socket_listen($this->sock, SOMAXCONN);
if ( !$result )
throw new Exception("Could not listen for connections $address:$port");
// if running as root and we made it here, switch credentials
if ( $socket_do_root )
{
posix_setuid($targetuser);
posix_setgid($targetgroup);
posix_setegid($targetgroup);
posix_seteuid($targetuser);
if ( function_exists('status') )
status('Successfully switched user ID');
}
$this->bind_address = $address;
$this->server_string = "PhpHttpd/" . HTTPD_VERSION . " PHP/" . PHP_VERSION . "\r\n";
$this->parent_pid = getmypid();
// create a UUID
$uuid_base = md5(microtime() . ( function_exists('mt_rand') ? mt_rand() : rand() ));
$this->uuid = substr($uuid_base, 0, 8) . '-' .
substr($uuid_base, 8, 4) . '-' .
substr($uuid_base, 12, 4) . '-' .
substr($uuid_base, 16, 4) . '-' .
substr($uuid_base, 20, 20);
}
/**
* Destructor.
*/
function __destruct()
{
if ( !defined('HTTPD_WS_CHILD') && $this->socket_initted )
{
if ( function_exists('status') )
status('WebServer: destroying socket');
// http://us3.php.net/manual/en/function.socket-bind.php
if ( !@socket_set_option($this->sock, SOL_SOCKET, SO_REUSEADDR, 1) )
{
echo socket_strerror(socket_last_error($sock)) . "\n";
}
@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 )
{
if ( function_exists('status') )
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);
$this->child_list[] = $pid;
continue;
}
else
{
// this is the child
define('HTTPD_WS_CHILD', 1);
@socket_set_option($this->sock, SOL_SOCKET, SO_REUSEADDR, 1);
socket_close($this->sock);
}
}
$this->in_keepalive = false;
$this->headers_sent = false;
$this->in_scriptlet = false;
// read request
$last_line = '';
$client_headers = '';
if ( defined('HTTPD_WS_CHILD') )
{
@socket_set_timeout($remote, HTTPD_KEEP_ALIVE_TIMEOUT);
}
if ( $line = @socket_read($remote, 1024, PHP_NORMAL_READ) )
{
do
{
$line = str_replace("\r", "", $line);
if ( empty($line) )
continue;
if ( $line == "\n" && $last_line == "\n" )
break;
$client_headers .= $line;
$last_line = $line;
}
while ( $line = @socket_read($remote, 1024, PHP_NORMAL_READ) );
}
else
{
if ( defined('HTTPD_WS_CHILD') )
{
$md = @socket_get_status($remote);
if ( @$md['timed_out'] )
{
status('[debug] keep-alive connection timed out');
continue; // will jump back to the start of the loop and kill the child process
}
}
}
// parse request
$client_headers = trim($client_headers);
if ( isset($last_finish_time) && empty($client_headers) && defined('HTTPD_WS_CHILD') && $last_finish_time + HTTPD_KEEP_ALIVE_TIMEOUT < microtime(true) )
{
status('[debug] keep-alive connection timed out (checkpoint 2)');
continue; // will jump back to the start of the loop and kill the child process
}
$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' );
}
// parse authorization, if any
if ( isset($_SERVER['PHP_AUTH_USER']) )
{
unset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
}
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);
}
// anything on POST?
$postdata = '';
$_POST = array();
$_FILES = array();
if ( $method == 'POST' )
{
// read POST data
if ( isset($_SERVER['HTTP_CONTENT_TYPE']) && preg_match('#^multipart/form-data; ?boundary=([A-z0-9_-]+)$#i', $_SERVER['HTTP_CONTENT_TYPE'], $match) )
{
// this is a multipart request
$boundary =& $match[1];
$mode = 'data';
$last_line = '';
$i = 0;
while ( $data = socket_read($remote, 8388608, PHP_NORMAL_READ) )
{
$data_trim = trim($data, "\r\n");
if ( $mode != 'data' )
{
$data = str_replace("\r", '', $data);
}
if ( ( $data_trim === "--$boundary" || $data_trim === "--$boundary--" ) && $i > 0 )
{
// trim off the first LF and the last CRLF
$currval_data = substr($currval_data, 1, strlen($currval_data)-3);
// this is the end of a part of the message; parse it into either $_POST or $_FILES
if ( is_string($have_a_file) )
{
// write data to a temporary file
$errcode = UPLOAD_ERR_OK;
$tempfile = tempnam('phpupload', ( function_exists('sys_get_temp_dir') ? sys_get_temp_dir() : '/tmp' ));
if ( $fh = @fopen($tempfile, 'w') )
{
if ( empty($have_a_file) )
{
$errcode = UPLOAD_ERR_NO_FILE;
}
else
{
fwrite($fh, $currval_data);
}
fclose($fh);
}
else
{
$errcode = UPLOAD_ERR_CANT_WRITE;
}
$_FILES[$currval_name] = array(
'name' => $have_a_file,
'type' => $currval_type,
'size' => filesize($tempfile),
'tmp_name' => $tempfile,
'error' => $errcode
);
}
else
{
$_POST[$currval_name] = $currval_data;
}
}
if ( $data_trim === "--$boundary" )
{
// switch from "data" mode to "headers" mode
$currval_name = '';
$currval_data = '';
$currval_type = '';
$have_a_file = false;
$mode = 'headers';
}
else if ( $data_trim === "--$boundary--" )
{
// end of request
break;
}
else if ( ( empty($data_trim) && empty($last_line) ) && $mode == 'headers' )
{
// start of data
$mode = 'data';
}
else if ( $mode == 'headers' )
{
// read header
// we're only looking for Content-Disposition and Content-Type
if ( preg_match('#^Content-Disposition: form-data; name="([^"\a\t\r\n]+)"(?:; filename="([^"\a\t\r\n]+)")?#i', $data_trim, $match) )
{
// content-disposition header, set name and mode.
$currval_name = $match[1];
if ( isset($match[2]) )
{
$have_a_file = $match[2];
}
else
{
$have_a_file = false;
}
}
else if ( preg_match('#^Content-Type: ([a-z0-9-]+/[a-z0-9/-]+)$#i', $data_trim, $match) )
{
$currval_type = $match[1];
}
}
else if ( $mode == 'data' )
{
$currval_data .= $data;
}
$last_line = $data_trim;
$i++;
}
}
else
{
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, '?'));
}
// set some server vars
$_SERVER['REQUEST_URI'] = '/' . rawurldecode($uri);
$_SERVER['REQUEST_METHOD'] = $method;
// get remote IP and port
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;
}
}
}
$_GET = $this->parse_multi_depth_array($_GET);
$_POST = $this->parse_multi_depth_array($_POST);
$_FILES = $this->parse_multi_depth_array($_FILES);
// init handler
$handler = false;
if ( $uri == '' )
{
// user requested the root (/). If there's a default document, use that; else, see if we can do a directory listing
$uri = strval($this->default_document);
if ( !$this->default_document && $this->allow_dir_list )
{
// we can list directories and this was requested by the user, so list it out
$handler = array('type' => 'rootdir');
}
}
$uri_parts = explode('/', $uri);
// hook for the special UUID handler
if ( $uri_parts[0] === $this->uuid && !$handler )
{
$handler = array('type' => 'sysuuid');
}
// loop through URI parts, see if a handler is set
if ( !$handler )
{
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 )
{
// try to make a fakie
if ( $this->check_for_handler_children($uri) )
{
$handler = array(
'type' => 'folder',
'dir' => "/{$this->uuid}/__fakie",
'id' => $uri
);
}
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);
// now that we're done sending the response, delete any temporary uploaded files
if ( !empty($_FILES) )
{
foreach ( $_FILES as $file_data )
{
if ( file_exists($file_data['tmp_name']) )
{
@unlink($file_data['tmp_name']);
}
}
}
if ( !$this->in_keepalive && defined('HTTPD_WS_CHILD') )
{
// if ( defined('HTTPD_WS_CHILD') )
// status('Closing connection');
@socket_shutdown($remote);
@socket_close($remote);
if ( function_exists('status') )
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");
$last_finish_time = microtime(true);
}
else
{
@socket_shutdown($remote);
@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;
if ( $this->headers_sent )
return false;
// this is reset after the request is completed (hopefully)
$this->headers_sent = true;
$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)';
if ( function_exists('status') )
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 'folder':
// 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_full = rtrim($uri, '/');
$uri = substr($uri, strlen($handler['id']) + 1);
// get file path
$file_path = rtrim($handler['dir'], '/') . $uri;
if ( file_exists($file_path) || $this->check_for_handler_children($uri_full) )
{
// found it :-D
// is this a directory?
if ( is_dir($file_path) || $this->check_for_handler_children($uri_full) )
{
// allowed to list?
if ( !$this->allow_dir_list )
{
$this->send_http_error($socket, 403, "Directory listing is not allowed.");
return true;
}
// yes, list contents
try
{
$dir_list = $this->list_directory($uri_full, true);
}
catch ( Exception $e )
{
$this->send_http_error($socket, 500, "Directory listing failed due to an error in the listing core method. This may indicate that the webserver process does not have filesystem access to the specified directory.<br /><br />Debugging details:<pre>$e</pre>");
return true;
}
$root = rtrim($uri_full, '/') . '/';
$parent = rtrim(dirname(rtrim($uri_full, '/')), '/') . '/';
$contents = <<<EOF
<html>
<head>
<title>Index of: $root</title>
<link rel="stylesheet" type="text/css" href="/{$this->uuid}/dirlist.css" />
</head>
<body>
<h1>Index of $root</h1>
<ul>
<li><tt><a href="$parent">Parent directory</a></tt></li>
EOF;
foreach ( $dir_list as $filename => $info )
{
$ts = ( $info['type'] == 'folder' ) ? '/' : '';
$contents .= ' <li><tt><a href="' . htmlspecialchars($root . basename($filename) . $ts) . '"><img alt="[ ]" src="/' . $this->uuid . '/' . $info['type'] . '.png" /> ' . htmlspecialchars($filename) . $ts . '</a></tt></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 'script':
// 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();
$this->in_scriptlet = true;
$result = @call_user_func($handler['function'], $this, $socket);
$this->in_scriptlet = false;
$output = ob_get_contents();
ob_end_clean();
}
// throw an HttpExceptionFatal when you need to break out of an in-progress scriptlet due to an error, use it in place of die() or exit()
catch ( HttpExceptionFatal $e )
{
ob_end_clean();
restore_error_handler();
$this->send_http_error($socket, 500, "A handler crashed reporting a fatal exception; see the command line for details.");
if ( function_exists('status') )
status("fatal exception in handler {$handler['id']}:\n$e");
return true;
}
catch ( HttpSuccess $e )
{
// just finish with success
$this->in_scriptlet = false;
$output = ob_get_contents();
ob_end_clean();
}
catch ( Exception $e )
{
ob_end_clean();
restore_error_handler();
$this->send_http_error($socket, 500, "There was an uncaught exception during the execution of a scripted handler function. See the command line for details.");
if ( function_exists('status') )
status("uncaught 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);
$this->headers_sent = false;
break;
case 'sysuuid':
// requested one of the system's icon images
$uri_parts = explode('/', $_SERVER['REQUEST_URI']);
if ( count($uri_parts) != 3 )
{
$this->send_http_error($socket, 404, "The requested URL " . htmlspecialchars($_SERVER['REQUEST_URI']) . " was not found on this server.");
}
// load image data
$filename =& $uri_parts[2];
switch ( $filename )
{
case 'script.png':
( !isset($image_data) ) ? $image_data = HTTPD_ICON_SCRIPT : null;
case 'folder.png':
( !isset($image_data) ) ? $image_data = HTTPD_ICON_FOLDER : null;
case 'file.png':
( !isset($image_data) ) ? $image_data = HTTPD_ICON_FILE : null;
$image_data = base64_decode($image_data);
$found = true;
$type = 'image/png';
break;
case 'dirlist.css':
$type = 'text/css';
$found = true;
$image_data = <<<EOF
/**
* PhpHttpd directory list visual style
*/
html {
background-color: #c9c9c9;
margin: 0;
padding: 0;
}
body {
background-color: #ffffff;
margin: 20px;
padding: 10px;
border: 1px solid #aaaaaa;
}
a {
text-decoration: none;
}
a img {
border-width: 0;
}
ul {
list-style-type: none;
}
EOF;
break;
default:
$found = false;
}
// ship it out
if ( $found )
{
$lm_date = date('r', filemtime(__FILE__));
$size = strlen($image_data);
$this->send_client_headers($socket, 200, $type, "Last-Modified: $lm_date\r\nContent-Length: $size");
@socket_write($socket, $image_data);
}
else
{
$this->send_http_error($socket, 404, "The requested URL " . htmlspecialchars($_SERVER['REQUEST_URI']) . " was not found on this server.");
}
return true;
break;
case 'rootdir':
//
// list the contents of the document root
//
$handlers = $this->list_directory('/', true);
$contents = <<<EOF
<html>
<head>
<title>Index of: /</title>
<link rel="stylesheet" type="text/css" href="/{$this->uuid}/dirlist.css" />
</head>
<body>
<h1>Index of /</h1>
<ul>
EOF;
$html = '';
// generate content
foreach ( $handlers as $uri => $handler )
{
switch($handler['type'])
{
case 'folder':
$image = 'folder.png';
$abbr = 'DIR';
$add = '/';
break;
case 'file':
default:
$image = 'file.png';
$abbr = ' ';
$add = '';
break;
case 'script':
$image = 'script.png';
$abbr = 'CGI';
$add = '';
break;
}
$html .= " <li><tt><a href=\"/$uri\"><img alt=\"[{$abbr}]\" src=\"/{$this->uuid}/{$image}\" /> {$uri}{$add}</a></tt></li>\n ";
}
$contents .= $html;
$contents .= <<<EOF
</ul>
<address>Served by {$this->server_string}</address>
</body>
</html>
EOF;
// get length
$len = strlen($contents);
// send headers
$this->send_client_headers($socket, 200, 'text/html', "Content-Length: $len");
// write to the socket
@socket_write($socket, $contents);
return true;
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';
// generate error page
$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;
// length of the response (required if we want keep-alive to work)
$this->header('Content-length: ' . strlen($html));
// if we're in a scriptlet, include custom headers
if ( $this->in_scriptlet )
$headers = implode("\r\n", $this->response_headers);
else
$headers = 'Content-length: ' . strlen($html);
$this->send_client_headers($socket, $http_code, 'text/html', $headers);
@socket_write($socket, $html);
}
/**
* 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)
{
if ( $type == 'dir' )
$type = 'folder';
if ( $type == 'function' )
$type = 'script';
switch($type)
{
case 'folder':
$this->handlers[$uri] = array(
'type' => 'folder',
'dir' => $value
);
break;
case 'file':
$this->handlers[$uri] = array(
'type' => 'file',
'file' => $value
);
break;
case 'script':
$this->handlers[$uri] = array(
'type' => 'script',
'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>';
}
/**
* Lists out the contents of a directory, including virtual handlers.
* @example
* Example return data: (will be ksorted)
<code>
array(
'bar' => 'folder',
'baz' => 'script',
'foo' => 'file'
);
</code>
* @param string Directory name, relative to the server's document root
* @param bool If true, sorts folders first (default: false)
* @return array Exception thrown on failure
*/
function list_directory($dir, $folders_first = false)
{
// clean slashes from the directory name
$dir = trim($dir, '/');
if ( $dir == '' )
{
//
// list the root, which can consist only of handlers
//
// copy the handlers array, which we need to ksort
$handlers = $this->handlers;
// get rid of multi-depth handlers
foreach ( $handlers as $uri => $handler )
{
if ( strpos($uri, '/') )
{
unset($handlers[$uri]);
$newuri = explode('/', $uri);
if ( !isset($handlers[$newuri[0]]) )
{
$handlers[$newuri[0]] = array(
'type' => 'folder'
);
}
}
}
ksort($handlers);
if ( $folders_first )
{
// sort folders first
$handlers_sorted = array();
foreach ( $handlers as $uri => $handler )
{
if ( $handler['type'] == 'folder' )
$handlers_sorted[$uri] = $handler;
}
foreach ( $handlers as $uri => $handler )
{
if ( $handler['type'] != 'folder' )
$handlers_sorted[$uri] = $handler;
}
$handlers = $handlers_sorted;
unset($handlers_sorted);
}
// done
return $handlers;
}
else
{
// list something within the root
$dir_stack = explode('/', $dir);
// lookup handler
$handler_search = $dir;
$found_handler = false;
$fake_handler = false;
$i = 1;
while ( $i > 0 )
{
if ( isset($this->handlers[$handler_search]) )
{
$found_handler = true;
break;
}
$i = strrpos($handler_search, '/');
$handler_search = substr($handler_search, 0, strrpos($handler_search, '/'));
}
if ( $this->check_for_handler_children($dir) )
{
$fake_handler = true;
}
else if ( !$found_handler )
{
// nope. not there.
throw new Exception("ERR_NO_SUCH_FILE_OR_DIRECTORY");
}
// make sure this is a directory
if ( !$fake_handler )
{
$handler =& $handler_search;
if ( $this->handlers[$handler]['type'] != 'folder' )
{
throw new Exception("ERR_NOT_A_DIRECTORY");
}
// determine real path
$real_path = realpath($this->handlers[$handler]['dir'] . substr($dir, strlen($handler)));
// directory is resolved; list contents
$dir_contents = array();
if ( $dr = opendir($real_path) )
{
while ( $dh = readdir($dr) )
{
if ( $dh == '.' || $dh == '..' )
{
continue;
}
$dir_contents[$dh] = array(
'type' => ( is_dir("$real_path/$dh") ) ? 'folder' : 'file',
'size' => filesize("$real_path/$dh"),
'time' => filemtime("$real_path/$dh")
);
}
}
else
{
// only if directory open failed
throw new Exception("ERR_PERMISSION_DENIED");
}
closedir($dr);
// some cleanup
unset($handler, $handler_search);
}
// list any additional handlers in there
foreach ( $this->handlers as $handler => $info )
{
// parse handler name
$handler_name = explode('/', trim($handler, '/'));
// is this handler in this directory?
if ( count($handler_name) != count($dir_stack) + 1 )
{
continue;
}
foreach ( $dir_stack as $i => $_ )
{
if ( $dir_stack[$i] != $handler_name[$i] )
{
continue 2;
}
}
// it's in here!
$dir_contents[ basename($handler) ] = array(
'type' => $info['type']
);
}
// list "fake" handlers
foreach ( $this->handlers as $handler => $info )
{
// parse handler name
$handler_name = explode('/', trim($handler, '/'));
// is this handler somewhere underneath this directory?
if ( count($handler_name) < count($dir_stack) + 2 )
{
continue;
}
// path check
foreach ( $dir_stack as $i => $_ )
{
if ( $dir_stack[$i] != $handler_name[$i] )
{
continue 2;
}
}
// create a "fake" directory
$fakie_name = $handler_name[ count($dir_stack) ];
$dir_contents[$fakie_name] = array(
'type' => 'folder'
);
}
if ( $folders_first )
{
// perform folder sorting
$unsorted = $dir_contents;
ksort($unsorted);
$dir_contents = array();
foreach ( $unsorted as $name => $info )
{
if ( $info['type'] == 'folder' )
$dir_contents[$name] = $info;
}
foreach ( $unsorted as $name => $info )
{
if ( $info['type'] != 'folder' )
$dir_contents[$name] = $info;
}
}
else
{
// not sorting with folders first, so just alphabetize
ksort($dir_contents);
}
// done
return $dir_contents;
}
}
/**
* Searches deeper to see if there are sub-handlers within a path to see if a fake handler can be created
* @param string URI
* @return bool
*/
function check_for_handler_children($file_path)
{
$file_path = trim($file_path, '/');
$dir_stack = explode('/', $file_path);
// make sure this isn't a "real" handler
if ( isset($this->handlers[$file_path]) )
{
return false;
}
// list any additional handlers in there
foreach ( $this->handlers as $handler => $info )
{
// parse handler name
$handler_name = explode('/', trim($handler, '/'));
// is this handler in this directory?
if ( count($handler_name) != count($dir_stack) + 1 )
{
continue;
}
foreach ( $dir_stack as $i => $_ )
{
if ( $dir_stack[$i] != $handler_name[$i] )
{
continue 2;
}
}
// it's in here!
return true;
}
return false;
}
/**
* Takes a flat array with keys of format foo[bar] and parses it into multiple depths.
* @param array
* @return array
*/
function parse_multi_depth_array($array)
{
foreach ( $array as $key => $value )
{
if ( preg_match('/^([^\[\]]+)\[([^\]]+)\]/', $key, $match) )
{
$parent =& $match[1];
$child =& $match[2];
if ( !isset($array[$parent]) || ( isset($array[$parent]) && !is_array($array[$parent]) ) )
{
$array[$parent] = array();
}
$array[$parent][$child] = $value;
unset($array[$key]);
$array[$parent] = $this->parse_multi_depth_array($array[$parent]);
}
}
return $array;
}
}
/**
* Exception class that allows breaking directly out of a scriptlet.
*/
class HttpExceptionFatal extends Exception
{
}
/**
* Exception class that will be treated as a scriptlet ending with success.
*/
class HttpSuccess extends Exception
{
}
/**
* 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 extension->mimetype 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'
);