# HG changeset patch # User Dan # Date 1214843773 14400 # Node ID 74edc873234fab3bdf5f48557c71ada02f59bd59 # Parent bb8237ca678de80451001c03fb36e74a4009c9db Made the webserver a bit smarter. It handles running as root properly (only allows it if user/group specified and port < 1024) and directory listing is massively smarter. diff -r bb8237ca678d -r 74edc873234f ajax.php --- a/ajax.php Fri Apr 25 14:56:52 2008 -0400 +++ b/ajax.php Mon Jun 30 12:36:13 2008 -0400 @@ -34,9 +34,32 @@ return true; } -function ajax_request_handler($httpd) +function ajax_request_handler($httpd, $socket) { global $playlist, $mime_types, $json, $allowcontrol; + global $use_auth, $auth_data; + + if ( $use_auth ) + { + if ( !isset($_SERVER['PHP_AUTH_USER']) ) + { + $httpd->header('WWW-Authenticate: basic'); + $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect."); + return true; + } + if ( !isset($auth_data[$_SERVER['PHP_AUTH_USER']]) ) + { + $httpd->header('WWW-Authenticate: basic'); + $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect."); + return true; + } + else if ( $_SERVER['PHP_AUTH_PW'] !== $auth_data[$_SERVER['PHP_AUTH_USER']] ) + { + $httpd->header('WWW-Authenticate: basic'); + $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect."); + return true; + } + } // Set content type $httpd->header("Content-type: {$mime_types['js']}"); @@ -117,8 +140,11 @@ rebuild_playlist(); } $current_track = dcop_action('playlist', 'getActiveIndex'); + $current_time = dcop_action('player', 'trackCurrentTime'); + $is_playing = dcop_action('player', 'isPlaying'); $return = array( - 'is_playing' => dcop_action('player', 'isPlaying'), + 'is_playing' => $is_playing, + 'is_paused' => $current_time > 0 && !$is_playing, 'current_track' => $current_track, 'volume' => dcop_action('player', 'getVolume'), // include the MD5 of the playlist so that if it changes, the @@ -131,7 +157,7 @@ if ( isset($playlist[$current_track]) ) { $return['current_track_length'] = $playlist[$current_track]['length_int']; - $return['current_track_pos'] = dcop_action('player', 'trackCurrentTime'); + $return['current_track_pos'] = $current_time; $return['current_track_title'] = $playlist[$current_track]['title']; $return['current_track_artist'] = $playlist[$current_track]['artist']; $return['current_track_album'] = $playlist[$current_track]['album']; diff -r bb8237ca678d -r 74edc873234f greyhound.php --- a/greyhound.php Fri Apr 25 14:56:52 2008 -0400 +++ b/greyhound.php Mon Jun 30 12:36:13 2008 -0400 @@ -40,7 +40,17 @@ // Allow forking when an HTTP request is received. This has advantages // and disadvantages. If this experimental option is enabled, it will // result in faster responses and load times but more memory usage. -$allow_fork = true; +$allow_fork = false; +// set to true to enable authentication +// WARNING: THIS HAS SOME SERIOUS SECURITY PROBLEMS RIGHT NOW. I don't +// know what's causing it to not prompt for authentication from any +// client after the first successful auth. +$use_auth = false; +// valid users and passwords +$auth_data = array( + 'funky' => 'monkey', + 'fast' => 'forward' + ); @ini_set('display_errors', 'on'); diff -r bb8237ca678d -r 74edc873234f playlist.php --- a/playlist.php Fri Apr 25 14:56:52 2008 -0400 +++ b/playlist.php Mon Jun 30 12:36:13 2008 -0400 @@ -13,9 +13,32 @@ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. */ -function amarok_playlist($server) +function amarok_playlist($httpd, $socket) { global $theme, $playlist, $allowcontrol; + global $use_auth, $auth_data; + + if ( $use_auth ) + { + if ( !isset($_SERVER['PHP_AUTH_USER']) ) + { + $httpd->header('WWW-Authenticate: basic'); + $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect."); + return true; + } + if ( !isset($auth_data[$_SERVER['PHP_AUTH_USER']]) ) + { + $httpd->header('WWW-Authenticate: basic'); + $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect."); + return true; + } + else if ( $auth_data[$_SERVER['PHP_AUTH_USER']] !== $_SERVER['PHP_AUTH_PW'] ) + { + $httpd->header('WWW-Authenticate: basic'); + $httpd->send_http_error($socket, 401, "A username and password are required to access this resource. Either you did not specify a username and password, or the supplied credentials were incorrect."); + return true; + } + } $iphone = ( ( strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') || strpos($_SERVER['HTTP_USER_AGENT'], 'iPod') || diff -r bb8237ca678d -r 74edc873234f scripts/ajax.js --- a/scripts/ajax.js Fri Apr 25 14:56:52 2008 -0400 +++ b/scripts/ajax.js Mon Jun 30 12:36:13 2008 -0400 @@ -187,7 +187,7 @@ updateTitle(response.current_track_artist, response.current_track_album, response.current_track_title); // if not playing, set the position slider to zero - if ( !is_playing ) + if ( !is_playing && !response.is_paused ) { posslide_set_position(0); } diff -r bb8237ca678d -r 74edc873234f webserver-icons/app.png Binary file webserver-icons/app.png has changed diff -r bb8237ca678d -r 74edc873234f webserver-icons/file.png Binary file webserver-icons/file.png has changed diff -r bb8237ca678d -r 74edc873234f webserver-icons/folder.png Binary file webserver-icons/folder.png has changed diff -r bb8237ca678d -r 74edc873234f webserver.php --- a/webserver.php Fri Apr 25 14:56:52 2008 -0400 +++ b/webserver.php Mon Jun 30 12:36:13 2008 -0400 @@ -21,6 +21,14 @@ define('HTTPD_VERSION', '0.1b1'); /** + * 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 web server written in PHP. * @package Amarok * @subpackage WebControl @@ -60,6 +68,13 @@ 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 */ @@ -110,12 +125,42 @@ 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; + + /** * 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) + function __construct($address = '127.0.0.1', $port = 8080, $targetuser = null, $targetgroup = null) { @set_time_limit(0); @ini_set('memory_limit', '256M'); @@ -126,17 +171,90 @@ 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')); + // 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); + $result = @socket_bind($this->sock, $address, $port); if ( !$result ) throw new Exception("Could not bind to $address:$port"); - $result = socket_listen($this->sock, SOMAXCONN); + $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"; + + // 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); } /** @@ -145,9 +263,15 @@ function __destruct() { - if ( !defined('HTTPD_WS_CHILD') ) + if ( !defined('HTTPD_WS_CHILD') && $this->socket_initted ) { - status('WebServer: destroying socket'); + 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); } @@ -164,7 +288,8 @@ // if this is a child process, we're finished - close up shop if ( defined('HTTPD_WS_CHILD') && !$this->in_keepalive ) { - status('Exiting child process'); + if ( function_exists('status') ) + status('Exiting child process'); @socket_shutdown($remote); @socket_close($remote); exit(0); @@ -211,11 +336,14 @@ { // 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 = ''; @@ -261,6 +389,11 @@ $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']; @@ -270,6 +403,7 @@ $_SERVER['PHP_AUTH_PW'] = substr(strstr($data, ':'), 1); } + // anything on POST? $postdata = ''; $_POST = array(); if ( $method == 'POST' ) @@ -303,8 +437,11 @@ $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(); @@ -319,31 +456,60 @@ } } + // 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 - $handler = false; - for ( $i = count($uri_parts) - 1; $i >= 0; $i-- ) + if ( !$handler ) { - $handler_test = implode('/', $uri_parts); - if ( isset($this->handlers[$handler_test]) ) + for ( $i = count($uri_parts) - 1; $i >= 0; $i-- ) { - $handler = $this->handlers[$handler_test]; - $handler['id'] = $handler_test; - break; + $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]); } - unset($uri_parts[$i]); } if ( !$handler ) { - $this->send_http_error($remote, 404, "The requested URL /$uri was not found on this server."); - continue; + // 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); @@ -354,7 +520,8 @@ // status('Closing connection'); @socket_shutdown($remote); @socket_close($remote); - status('Exiting child process'); + if ( function_exists('status') ) + status('Exiting child process'); exit(0); } else if ( defined('HTTPD_WS_CHILD') ) @@ -363,6 +530,11 @@ // status('Continuing connection'); // @socket_write($remote, "\r\n\r\n"); } + else + { + @socket_shutdown($remote); + @socket_close($remote); + } } } @@ -377,10 +549,18 @@ 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)'; - status("{$_SERVER['REMOTE_ADDR']} {$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']} $http_code {$_SERVER['HTTP_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); @@ -408,7 +588,7 @@ { switch ( $handler['type'] ) { - case 'dir': + case 'folder': // security $uri = str_replace("\000", '', $_SERVER['REQUEST_URI']); if ( preg_match('#(\.\./|\/\.\.)#', $uri) || strstr($uri, "\r") || strstr($uri, "\n") ) @@ -420,58 +600,55 @@ 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) ) + if ( file_exists($file_path) || $this->check_for_handler_children($uri_full) ) { // found it :-D // is this a directory? - if ( is_dir($file_path) ) + 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 - $root = '/' . $handler['id'] . rtrim($uri, '/'); - $parent = substr($root, 0, strrpos($root, '/')) . '/'; - + 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.

Debugging details:
$e
"); + return true; + } + + $root = rtrim($uri_full, '/') . '/'; + $parent = rtrim(dirname(rtrim($uri_full, '/')), '/') . '/'; + $contents = << Index of: $root +

Index of $root

\n
Served by {$this->server_string}
\n\n\n\n"; @@ -567,7 +744,7 @@ } break; - case 'function': + case 'script': // init vars $this->content_type = 'text/html'; $this->response_code = 200; @@ -578,7 +755,9 @@ try { ob_start(); - $result = @call_user_func($handler['function'], $this); + $this->in_scriptlet = true; + $result = @call_user_func($handler['function'], $this, $socket); + $this->in_scriptlet = false; $output = ob_get_contents(); ob_end_clean(); } @@ -586,7 +765,8 @@ { 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"); + if ( function_exists('status') ) + status("caught exception in handler {$handler['id']}:\n$e"); return true; } restore_error_handler(); @@ -610,6 +790,150 @@ // 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 = <<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 = << + + Index of: / + + + +

Index of /

+
    + +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 .= "
  • \"[{$abbr}]\"uuid}/{$image}\" /> {$uri}{$add}
  • \n "; + } + + $contents .= $html; + $contents .= << +
    Served by {$this->server_string}
    + + +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; } } @@ -646,7 +970,14 @@ { global $http_responses; $reason_code = ( isset($http_responses[$http_code]) ) ? $http_responses[$http_code] : 'Unknown'; - $this->send_client_headers($socket, $http_code); + + // if we're in a scriptlet, include custom headers + if ( $this->in_scriptlet ) + $headers = implode("\r\n", $this->response_headers); + else + $headers = ''; + + $this->send_client_headers($socket, $http_code, 'text/html', $headers); $html = << @@ -661,8 +992,7 @@ EOF; @socket_write($socket, $html); - @socket_close($socket); - } + } /** * Adds a new handler @@ -673,11 +1003,15 @@ function add_handler($uri, $type, $value) { + if ( $type == 'dir' ) + $type = 'folder'; + if ( $type == 'function' ) + $type = 'script'; switch($type) { - case 'dir': + case 'folder': $this->handlers[$uri] = array( - 'type' => 'dir', + 'type' => 'folder', 'dir' => $value ); break; @@ -687,9 +1021,9 @@ 'file' => $value ); break; - case 'function': + case 'script': $this->handlers[$uri] = array( - 'type' => 'function', + 'type' => 'script', 'function' => $value ); break; @@ -709,6 +1043,265 @@ echo ''; } + /** + * Lists out the contents of a directory, including virtual handlers. + * @example + * Example return data: (will be ksorted) + + array( + 'bar' => 'folder', + 'baz' => 'script', + 'foo' => 'file' + ); + + * @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; + } + } /**