--- a/webserver.php Sun Aug 24 01:26:20 2008 -0400
+++ b/webserver.php Sun Aug 24 01:28:52 2008 -0400
@@ -36,6 +36,12 @@
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=');
/**
+ * Abstraction layer to use
+ */
+
+define('HTTPD_SOCKET_LAYER', 'Stream');
+
+/**
* Simple but full-featured embedded web server written in PHP.
* @package Amarok
* @subpackage WebControl
@@ -54,11 +60,11 @@
var $bind_address = '127.0.0.1';
/**
- * Socket resource
- * @var resource
+ * Socket abstraction object
+ * @var object
*/
- var $sock = null;
+ var $server = null;
/**
* Server string
@@ -174,6 +180,15 @@
var $parent_pid = 0;
/**
+ * Sockets for parent and child to communicate
+ * @var resource
+ * @var resource
+ */
+
+ var $parent_sock = null;
+ var $child_sock = null;
+
+ /**
* Constructor.
* @param string IPv4 address to bind to
* @param int Port number
@@ -244,16 +259,9 @@
}
$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");
+ $class = 'Socket_' . HTTPD_SOCKET_LAYER;
+ $this->server = new $class();
+ $this->server->tcp_listen($address, $port);
// if running as root and we made it here, switch credentials
if ( $socket_do_root )
@@ -277,6 +285,7 @@
substr($uuid_base, 12, 4) . '-' .
substr($uuid_base, 16, 4) . '-' .
substr($uuid_base, 20, 20);
+
}
/**
@@ -289,13 +298,24 @@
{
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) )
+ $this->server->destroy();
+
+ // tell all children to shut down
+ if ( $this->allow_fork )
{
- echo socket_strerror(socket_last_error($sock)) . "\n";
+ if ( function_exists('status') )
+ status('WebServer: asking all children to exit');
+ $this->send_ipc_event("die _");
}
- @socket_shutdown($this->sock, 2);
- @socket_close($this->sock);
+
+ // that last operation should have been asynchronous, so shut everything down now
+ @socket_shutdown($this->parent_sock);
+ @socket_close($this->parent_sock);
+ }
+ else if ( defined('HTTPD_WS_CHILD') )
+ {
+ @socket_shutdown($this->child_sock);
+ @socket_close($this->child_sock);
}
}
@@ -305,6 +325,24 @@
function serve()
{
+ // If we're allowed to use multithreading, set up to handle SIGUSR2 which waits on the child
+ if ( function_exists('pcntl_signal') && $this->allow_fork )
+ {
+ // required for signal handling to work
+ declare(ticks=1);
+
+ // trap SIGTERM
+ pcntl_signal(SIGUSR2, array(&$this, '_ipc_event'));
+
+ if ( !($sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP)) )
+ {
+ throw new Exception("Could not set up private IPC socket. Reason: " . socket_strerror(socket_last_error()));
+ }
+
+ $this->parent_sock =& $sockets[0];
+ $this->child_sock =& $sockets[1];
+ }
+
while ( true )
{
// if this is a child process, we're finished - close up shop
@@ -312,8 +350,13 @@
{
if ( function_exists('status') )
status('Exiting child process');
- @socket_shutdown($remote);
- @socket_close($remote);
+
+ $remote->destroy();
+
+ // let the parent know that we're out of here
+ $this->send_ipc_event("exit " . getmypid());
+
+ // bye
exit(0);
}
@@ -321,19 +364,9 @@
// 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;
- }
+ $remote = $this->server->accept();
}
-
+
if ( !$remote )
{
$this->in_keepalive = false;
@@ -351,7 +384,7 @@
else if ( $pid )
{
// we are the parent, continue listening
- socket_close($remote);
+ $remote->soft_shutdown();
$this->child_list[] = $pid;
continue;
}
@@ -359,8 +392,16 @@
{
// this is the child
define('HTTPD_WS_CHILD', 1);
- @socket_set_option($this->sock, SOL_SOCKET, SO_REUSEADDR, 1);
- socket_close($this->sock);
+
+ // setup to handle signals
+ if ( function_exists('pcntl_signal') )
+ {
+ // required for signal handling to work
+ declare(ticks=1);
+
+ // trap SIGTERM
+ pcntl_signal(SIGUSR2, array(&$this, '_ipc_event'));
+ }
}
}
@@ -368,39 +409,77 @@
$this->headers_sent = false;
$this->in_scriptlet = false;
- // read request
+ //
+ // READ THE REQUEST
+ //
+
+ // this is a complicated situation because we need to keep enough ticks going to properly handle
+ // signals, meaning we can't use stream_set_timeout() and instead need to rely on our own timing
+ // logic. setting the timeout to a short period, say 200,000 usec, we can minimize CPU usage and
+ // have a good response time.
+
+ $remote->set_timeout(0, 200000);
+ $start_time = microtime(true);
+ $client_headers = '';
+ $last_line = '';
+ while ( true )
+ {
+ if ( $start_time + HTTPD_KEEP_ALIVE_TIMEOUT < microtime(true) || $remote->is_eof() )
+ {
+ // request expired -- end the process here
+ if ( !defined('HTTPD_WS_CHILD') )
+ $remote->destroy();
+
+ continue 2;
+ }
+ $line = strval($remote->read_normal());
+ $line = str_replace("\r", "", $line);
+
+ // raw layer wants to send 2 empty lines through, stream layer wants to send one.
+ $last_line_check = ( HTTPD_SOCKET_LAYER != 'Raw' || ( HTTPD_SOCKET_LAYER == 'Raw' && $last_line == "\n" ) );
+ if ( $line == "\n" && $last_line_check )
+ // we have two newlines in a row, break out since we have a full request
+ break;
+
+ $client_headers .= $line;
+ $last_line = $line;
+ }
+
+ /*
$last_line = '';
$client_headers = '';
if ( defined('HTTPD_WS_CHILD') )
{
- @socket_set_timeout($remote, HTTPD_KEEP_ALIVE_TIMEOUT);
+ $remote->set_timeout(1);
}
- if ( $line = @socket_read($remote, 1024, PHP_NORMAL_READ) )
+ if ( $line = $remote->read_normal() )
{
do
{
$line = str_replace("\r", "", $line);
if ( empty($line) )
continue;
- if ( $line == "\n" && $last_line == "\n" )
+ $last_line_check = ( HTTPD_SOCKET_LAYER != 'Raw' || ( HTTPD_SOCKET_LAYER == 'Raw' && $last_line == "\n" ) );
+ if ( $line == "\n" && $last_line_check )
break;
$client_headers .= $line;
$last_line = $line;
+ $line = $remote->read_normal()
}
- while ( $line = @socket_read($remote, 1024, PHP_NORMAL_READ) );
+ while ( true );
}
else
{
if ( defined('HTTPD_WS_CHILD') )
{
- $md = @socket_get_status($remote);
- if ( @$md['timed_out'] )
+ if ( $remote->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);
@@ -467,7 +546,7 @@
$mode = 'data';
$last_line = '';
$i = 0;
- while ( $data = socket_read($remote, 8388608, PHP_NORMAL_READ) )
+ while ( $data = $remote->read_normal(8388608) )
{
$data_trim = trim($data, "\r\n");
if ( $mode != 'data' )
@@ -569,11 +648,11 @@
{
if ( isset($_SERVER['HTTP_CONTENT_LENGTH']) )
{
- $postdata = socket_read($remote, intval($_SERVER['HTTP_CONTENT_LENGTH']), PHP_BINARY_READ);
+ $postdata = $remote->read_binary(intval($_SERVER['HTTP_CONTENT_LENGTH']));
}
else
{
- $postdata = socket_read($remote, 8388608, PHP_NORMAL_READ);
+ $postdata = $remote->read_normal(8388608);
}
if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]-]+)(=[^ &]+)?/', $postdata, $matches) )
{
@@ -601,7 +680,7 @@
$_SERVER['REQUEST_METHOD'] = $method;
// get remote IP and port
- socket_getpeername($remote, $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT']);
+ $remote->get_peer_info($_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT']);
$_GET = array();
if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]-]+)(=[^ &]+)?/', $params, $matches) )
@@ -691,25 +770,20 @@
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);
+ // connection: close
+ // continue on to the shutdown handler
+ continue;
}
else if ( defined('HTTPD_WS_CHILD') )
{
// if ( defined('HTTPD_WS_CHILD') )
// status('Continuing connection');
- // @socket_write($remote, "\r\n\r\n");
+ // $remote->write("\r\n\r\n");
$last_finish_time = microtime(true);
}
else
{
- @socket_shutdown($remote);
- @socket_close($remote);
+ $remote->destroy();
}
}
}
@@ -743,15 +817,15 @@
$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");
+ $socket->write("HTTP/1.1 $http_code $reason_code\r\n");
+ $socket->write("Server: $this->server_string");
+ $socket->write("Connection: $connection\r\n");
+ $socket->write("Content-Type: $contenttype\r\n");
if ( !empty($headers) )
{
- @socket_write($socket, "$headers\r\n");
+ $socket->write("$headers\r\n");
}
- @socket_write($socket, "\r\n");
+ $socket->write("\r\n");
}
/**
@@ -831,7 +905,7 @@
$sz = strlen($contents);
$this->send_client_headers($socket, 200, 'text/html', "Content-length: $sz\r\n");
- @socket_write($socket, $contents);
+ $socket->write($contents);
return true;
}
@@ -858,7 +932,7 @@
// send body
while ( $blk = @fread($fh, 768000) )
{
- @socket_write($socket, $blk);
+ $socket->write($blk);
}
fclose($fh);
return true;
@@ -909,7 +983,7 @@
// send body
while ( $blk = @fread($fh, 768000) )
{
- @socket_write($socket, $blk);
+ $socket->write($blk);
}
fclose($fh);
return true;
@@ -983,7 +1057,7 @@
// $output = dechex(strlen($output)) . "\r\n$output";
// write body
- @socket_write($socket, $output);
+ $socket->write($output);
$this->headers_sent = false;
@@ -1056,7 +1130,7 @@
$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);
+ $socket->write($image_data);
}
else
{
@@ -1126,7 +1200,7 @@
$this->send_client_headers($socket, 200, 'text/html', "Content-Length: $len");
// write to the socket
- @socket_write($socket, $contents);
+ $socket->write($contents);
return true;
break;
@@ -1191,7 +1265,7 @@
$headers = 'Content-length: ' . strlen($html);
$this->send_client_headers($socket, $http_code, 'text/html', $headers);
- @socket_write($socket, $html);
+ $socket->write($html);
}
/**
@@ -1528,6 +1602,286 @@
return $array;
}
+ /**
+ * Handle an IPC event. Called only upon SIGUSR2.
+ */
+
+ function _ipc_event()
+ {
+ $pid = getmypid() . ':' . $this->parent_pid;
+
+ // decide which socket to use
+ if ( defined('HTTPD_WS_CHILD') )
+ $sock =& $this->parent_sock;
+ else
+ $sock =& $this->child_sock;
+
+ // try to read the event
+ // this sometimes gets hung up because socket_set_timeout() doesn't seem to work on its own set of
+ // functions (it only works on PHP's normal streams)
+ if ( $line = @fgets($sock, 1024) )
+ {
+ $line = trim($line);
+ list($action, $param) = explode(' ', $line);
+ switch($action)
+ {
+ case 'exit':
+ // this is to prevent zombie children
+ pcntl_waitpid(intval($param), $status);
+ // we know this child is dead now, remove them from the list
+ foreach ( $this->child_list as $i => $pid )
+ {
+ if ( $pid === intval($param) )
+ {
+ unset($this->child_list[$i]);
+ $this->child_list = array_values($this->child_list);
+ break;
+ }
+ }
+ break;
+ case 'die':
+ // only do this if this is a child (both security and design)
+ if ( defined('HTTPD_WS_CHILD') )
+ {
+ if ( function_exists('status') )
+ {
+ status('Received shutdown request, complying');
+ }
+ $this->send_ipc_event("exit " . getmypid());
+ exit(0);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ /**
+ * Send an IPC event.
+ * @param string Data to write to the socket, newline will be added automatically
+ */
+
+ function send_ipc_event($data)
+ {
+ if ( defined('HTTPD_WS_CHILD') )
+ $sock =& $this->parent_sock;
+ else
+ $sock =& $this->child_sock;
+
+ $data = rtrim($data, "\r\n") . "\n";
+ @fwrite($sock, $data);
+
+ // if we're a child, signal the parent
+ if ( defined('HTTPD_WS_CHILD') )
+ {
+ posix_kill($this->parent_pid, SIGUSR2);
+ }
+ // if we're the parent, signal all children
+ else
+ {
+ foreach ( $this->child_list as $pid )
+ {
+ posix_kill($pid, SIGUSR2);
+ }
+ }
+ }
+
+}
+
+/**
+ * Socket abstraction layer - low-level socket functions (socket_*)
+ */
+
+class Socket_Raw
+{
+ var $sock;
+ var $socket_initted = false;
+
+ function tcp_listen($address, $port)
+ {
+ $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");
+
+ $this->socket_initted = true;
+ }
+
+ function destroy()
+ {
+ if ( $this->socket_initted )
+ {
+ // 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($this->sock)) . "\n";
+ }
+ @socket_shutdown($this->sock, 2);
+ @socket_close($this->sock);
+ }
+ }
+
+ function accept()
+ {
+ $remote = false;
+ $timeout = 5;
+ switch(@socket_select($r = array($this->sock), $w = array($this->sock), $e = array($this->sock), $timeout)) {
+ case 2:
+ return false;
+ case 1:
+ $remote = @socket_accept($this->sock);
+ $return = new Socket_Raw();
+ $return->sock = $remote;
+ $return->socket_initted = true;
+ return $return;
+ break;
+ case 0:
+ return false;
+ }
+ }
+
+ /**
+ * Closes the socket but doesn't destroy it.
+ */
+
+ function soft_shutdown()
+ {
+ @socket_set_option($this->sock, SOL_SOCKET, SO_REUSEADDR, 1);
+ socket_close($this->sock);
+ }
+
+ function set_timeout($timeout, $usec = false)
+ {
+ // doesn't work in this.
+ }
+
+ function read_normal($length = 1024)
+ {
+ return @socket_read($this->sock, $length, PHP_NORMAL_READ);
+ }
+
+ function read_binary($length = 1024)
+ {
+ return @socket_read($this->sock, $length, PHP_BINARY_READ);
+ }
+
+ function timed_out()
+ {
+ $md = @socket_get_status($this->sock);
+ return ( isset($md['timed_out']) ) ? $md['timed_out'] : false;
+ }
+
+ function get_peer_info(&$addr, &$port)
+ {
+ socket_getpeername($this->sock, $addr, $port);
+ }
+
+ function write($data)
+ {
+ return @socket_write($this->sock, $data);
+ }
+
+ function is_eof()
+ {
+ // feof() not supported
+ return false;
+ }
+}
+
+/**
+ * Socket abstraction layer - PHP stream support
+ */
+
+class Socket_Stream
+{
+ var $sock;
+ var $socket_initted = false;
+
+ function tcp_listen($address, $port)
+ {
+ $this->sock = @stream_socket_server("tcp://$address:$port", $errno, $errstr);
+ if ( !$this->sock )
+ throw new Exception("Could not create the socket: error $errno: $errstr");
+ }
+
+ function destroy()
+ {
+ if ( $this->socket_initted )
+ {
+ // PHP >= 5.2.1
+ if ( function_exists('stream_socket_shutdown') )
+ {
+ @stream_socket_shutdown($this->sock, STREAM_SHUT_RDWR);
+ }
+ fclose($this->sock);
+ }
+ }
+
+ function accept()
+ {
+ // the goal of a custom accept() with *_select() is to tick every 5 seconds to allow signals.
+ stream_set_blocking($this->sock, 1);
+ $timeout = 5;
+ $selection = @stream_select($r = array($this->sock), $w = array($this->sock), $e = array($this->sock), $timeout);
+ if ( !$selection )
+ {
+ return false;
+ }
+ $remote = stream_socket_accept($this->sock);
+ $return = new Socket_Stream();
+ $return->sock = $remote;
+ $return->socket_initted = true;
+ return $return;
+ }
+
+ function soft_shutdown()
+ {
+ fclose($this->sock);
+ }
+
+ function set_timeout($timeout, $usec = false)
+ {
+ return ( $usec ) ? @stream_set_timeout($this->sock, 0, $usec) : @stream_set_timeout($this->sock, $timeout);
+ }
+
+ function read_normal($length = 1024)
+ {
+ return @fgets($this->sock, $length);
+ }
+
+ function read_binary($length = 1024)
+ {
+ return @fread($this->sock, $length);
+ }
+
+ function timed_out()
+ {
+ $md = @stream_get_meta_data($this->sock);
+ return ( isset($md['timed_out']) ) ? $md['timed_out'] : false;
+ }
+
+ function get_peer_info(&$addr, &$port)
+ {
+ $peer = stream_socket_get_name($this->sock, true);
+ list($addr, $port) = explode(':', $peer);
+ }
+
+ function write($data)
+ {
+ return @fwrite($this->sock, $data);
+ }
+
+ function is_eof()
+ {
+ return feof($this->sock);
+ }
}
/**