diff -r 000000000000 -r c63de9eb7045 webserver.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/webserver.php Sun Mar 23 14:59:33 2008 -0400 @@ -0,0 +1,638 @@ +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 = << + + Index of: $root + + +

Index of $root

+ \n
Served by {$this->server_string}
\n\n\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 = << + + $http_code $reason_code + + +

$http_code $reason_code

+

$errstring

+
+
Served by $this->server_string
+ + +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 + */ + + function function_error_handler($errno, $errstr, $errfile, $errline, $errcontext) + { + echo '
'; + echo "PHP warning/error: type $errno ($errstr) caught in $errfile on $errline
"; + echo "Error context:
" . htmlspecialchars(print_r($errcontext, true)) . "
"; + echo '
'; + } + +} + +/** + * 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' + ); +