# HG changeset patch # User Dan # Date 1214900266 14400 # Node ID 08225c2eb0b6b7f11e7790d0be4d12775db0eb86 # Parent 3a4f0cd0794ee906be98ca4c5ce81b209a216233 Added support for multipart forms, file uploads, and fatal exceptions in PhpHttpd; fixed wrong license tag on WebServer class diff -r 3a4f0cd0794e -r 08225c2eb0b6 webserver.php --- a/webserver.php Tue Jul 01 04:16:04 2008 -0400 +++ b/webserver.php Tue Jul 01 04:17:46 2008 -0400 @@ -33,7 +33,7 @@ * @package Amarok * @subpackage WebControl * @author Dan Fuhry - * @license Public domain + * @license GNU General Public License */ class WebServer @@ -163,7 +163,7 @@ function __construct($address = '127.0.0.1', $port = 8080, $targetuser = null, $targetgroup = null) { @set_time_limit(0); - @ini_set('memory_limit', '256M'); + @ini_set('memory_limit', '128M'); // do we have socket functions? if ( !function_exists('socket_create') ) @@ -406,24 +406,133 @@ // anything on POST? $postdata = ''; $_POST = array(); + $_FILES = array(); if ( $method == 'POST' ) { // read POST data - if ( isset($_SERVER['HTTP_CONTENT_LENGTH']) ) + if ( isset($_SERVER['HTTP_CONTENT_TYPE']) && preg_match('#^multipart/form-data; ?boundary=([A-z0-9_-]+)$#i', $_SERVER['HTTP_CONTENT_TYPE'], $match) ) { - $postdata = socket_read($remote, intval($_SERVER['HTTP_CONTENT_LENGTH']), PHP_BINARY_READ); + // 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 { - $postdata = socket_read($remote, 8388608, PHP_NORMAL_READ); - } - if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]-]+)(=[^ &]+)?/', $postdata, $matches) ) - { - if ( isset($matches[1]) ) + if ( isset($_SERVER['HTTP_CONTENT_LENGTH']) ) + { + $postdata = socket_read($remote, intval($_SERVER['HTTP_CONTENT_LENGTH']), PHP_BINARY_READ); + } + else { - foreach ( $matches[0] as $i => $_ ) + $postdata = socket_read($remote, 8388608, PHP_NORMAL_READ); + } + if ( preg_match_all('/(^|&)([a-z0-9_\.\[\]-]+)(=[^ &]+)?/', $postdata, $matches) ) + { + if ( isset($matches[1]) ) { - $_POST[$matches[2][$i]] = ( !empty($matches[3][$i]) ) ? urldecode(substr($matches[3][$i], 1)) : true; + foreach ( $matches[0] as $i => $_ ) + { + $_POST[$matches[2][$i]] = ( !empty($matches[3][$i]) ) ? urldecode(substr($matches[3][$i], 1)) : true; + } } } } @@ -456,6 +565,10 @@ } } + $_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; @@ -514,6 +627,18 @@ $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') ) @@ -761,12 +886,21 @@ $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 ) + { + 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 ( Exception $e ) { restore_error_handler(); - $this->send_http_error($socket, 500, "A handler crashed with an exception; see the command line for details."); + $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("caught exception in handler {$handler['id']}:\n$e"); + status("uncaught exception in handler {$handler['id']}:\n$e"); return true; } restore_error_handler(); @@ -1302,6 +1436,40 @@ 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 +{ } /**