From 5d30ace42b34e1f5c10e30838d816aa9c4b4de1b Mon Sep 17 00:00:00 2001 From: Shahar Evron Date: Sat, 21 Mar 2009 20:41:13 +0200 Subject: [PATCH] Initial semi-working code commit (one request is ok, two - not so much...) --- .gitignore | 3 + README | 3 + examples/server.php | 10 + library/Aspamia/Exception.php | 7 + library/Aspamia/Http/Exception.php | 9 + library/Aspamia/Http/Message.php | 186 ++++++++ library/Aspamia/Http/Request.php | 213 +++++++++ library/Aspamia/Http/Response.php | 417 ++++++++++++++++++ library/Aspamia/Http/Server.php | 117 +++++ library/Aspamia/Http/Server/Exception.php | 8 + .../Aspamia/Http/Server/Handler/Abstract.php | 36 ++ library/Aspamia/Http/Server/Handler/Cgi.php | 3 + .../Aspamia/Http/Server/Handler/Exception.php | 8 + library/Aspamia/Http/Server/Handler/Mock.php | 32 ++ .../Aspamia/Http/Server/Handler/Static.php | 69 +++ library/Zend/Exception.php | 30 ++ library/Zend/Loader.php | 263 +++++++++++ 17 files changed, 1414 insertions(+) create mode 100644 .gitignore create mode 100644 README create mode 100644 examples/server.php create mode 100644 library/Aspamia/Exception.php create mode 100644 library/Aspamia/Http/Exception.php create mode 100644 library/Aspamia/Http/Message.php create mode 100644 library/Aspamia/Http/Request.php create mode 100644 library/Aspamia/Http/Response.php create mode 100644 library/Aspamia/Http/Server.php create mode 100644 library/Aspamia/Http/Server/Exception.php create mode 100644 library/Aspamia/Http/Server/Handler/Abstract.php create mode 100644 library/Aspamia/Http/Server/Handler/Cgi.php create mode 100644 library/Aspamia/Http/Server/Handler/Exception.php create mode 100644 library/Aspamia/Http/Server/Handler/Mock.php create mode 100644 library/Aspamia/Http/Server/Handler/Static.php create mode 100644 library/Zend/Exception.php create mode 100644 library/Zend/Loader.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5972dba --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.project +.settings/ + diff --git a/README b/README new file mode 100644 index 0000000..83f9647 --- /dev/null +++ b/README @@ -0,0 +1,3 @@ +Aspamia HTTP Server Library for PHP +----------------------------------- +(c) 2009 Shahar Evron, all rights reserved diff --git a/examples/server.php b/examples/server.php new file mode 100644 index 0000000..5d84201 --- /dev/null +++ b/examples/server.php @@ -0,0 +1,10 @@ +run(); \ No newline at end of file diff --git a/library/Aspamia/Exception.php b/library/Aspamia/Exception.php new file mode 100644 index 0000000..c5a4069 --- /dev/null +++ b/library/Aspamia/Exception.php @@ -0,0 +1,7 @@ + + */ + +/** + * Abstract HTTP message class - + * + * The HTTP message class defines the common methods and properties of HTTP + * request and response messages, and provides a set of utility functions for + * handling HTTP messages. + * + */ +abstract class Aspamia_Http_Message +{ + const CRLF = "\r\n"; + + /** + * Message headers + * + * @var array + */ + protected $_headers = array(); + + /** + * Message body + * + * @var string + */ + protected $_body = null; + + /** + * HTTP version (1.1 or 1.0) + * + * @var string + */ + protected $_httpVersion = '1.1'; + + /** + * Get the body of the message as a string + * + * @return string + */ + public function getBody() + { + return $this->_body; + } + + /** + * Get a specific header by it's name + * + * @param string $header + * @return string + */ + public function getHeader($header) + { + $header = strtolower($header); + if (isset($this->_headers[$header])) { + return $this->_headers[$header]; + } else { + return null; + } + } + + /** + * Get the array of all headers, or the entire headers section as a string + * + * @return array | string + */ + public function getAllHeaders($as_string = false) + { + if ($as_string) { + $str = $this->_getStartLine() . self::CRLF; + foreach ($this->_headers as $header => $value) { + $str .= ucwords($header) . ": " . $value . self::CRLF; + } + return $str; + + } else { + return $this->_headers; + } + } + + /** + * Get the HTTP version (1.0, 1.1 etc.) + * + * @return string + */ + public function getHttpVersion() + { + return $this->_httpVersion; + } + + /** + * Set the body of the message + * + * @param string $_body + */ + public function setBody($_body) + { + $this->_body = $_body; + } + + /** + * Set a single header or multiple headers passed as an array + * + * @param string | array $header + * @param string $value + */ + public function setHeader($header, $value = null) + { + // Handle an array of headers by simply passing them one by one to + // this function + if (is_array($header)) { + foreach($header as $k => $v) { + if (is_int($k)) { + $this->setHeader($v); + } else { + $this->setHeader($k, $v); + } + } + + // If we got a single header, set it + } else { + + // No value - expect a single 'key: value' string + if ($value === null) { + $parts = explode(':', $header, 2); + if (count($parts) != 2) { + require_once 'Aspamia/Http/Exception.php'; + throw new Aspamia_Http_Exception("Invalid HTTP header: '$header'"); + } + + $header = trim($parts[0]); + $value = trim($parts[1]); + } + + $header = strtolower($header); + + // Validate header name - this is not exactly according to the RFC + // but should usually work in reality (if not, we can fix it ;) + if (! preg_match('/^[a-z0-9\-]+$/', $header)) { + require_once 'Aspamia/Http/Exception.php'; + throw new Aspamia_Http_Exception("Invalid HTTP header name: '$header'"); + } + + if (isset($this->_headers[$header])) { + // RFC allows us to join multiple headers and comma separate them + $this->_headers[$header] .= ", $value"; + } else { + $this->_headers[$header] = $value; + } + } + } + + /** + * Set the HTTP version + * + * @param string $_httpVersion + */ + public function setHttpVersion($_httpVersion) + { + if (! ($_httpVersion == '1.0' || $_httpVersion == '1.1')) { + require_once 'Aspamia/Http/Exception.php'; + throw new Aspamia_Http_Exception("Unsupported HTTP version: $_httpVersion"); + } + + $this->_httpVersion = $_httpVersion; + } + + abstract protected function _getStartLine(); + + /** + * Stringify the message object. This could usually be sent over the wire. + * + * @return string + */ + public function __toString() + { + return $this->getAllHeaders(true) . self::CRLF . $this->getBody(); + } +} \ No newline at end of file diff --git a/library/Aspamia/Http/Request.php b/library/Aspamia/Http/Request.php new file mode 100644 index 0000000..70b3859 --- /dev/null +++ b/library/Aspamia/Http/Request.php @@ -0,0 +1,213 @@ +setMethod($method); + $this->setUri($uri); + $this->setHeader($headers); + $this->setBody($body); + } + + /** + * Get the request method + * + * @return string + */ + public function getMethod() + { + return $this->_method; + } + + /** + * Get the request URI + * + * @return string + */ + public function getUri() + { + return $this->_uri; + } + + /** + * Set the request method + * + * @param string $method + * @return Aspamia_Http_Request + */ + public function setMethod($method) + { + // Validate the method - this is not RFC complient but it's close + // enough. If people will complain we can fix it ;) + + if (! preg_match('/^[\w\-]+$/', $method)) { + require_once 'Aspamia/Http/Exception.php'; + throw new Aspamia_Http_Exception("Invalid HTTP method: '$method'"); + } + + $this->_method = $method; + return $this; + } + + /** + * Validate and set the request URI + * + * @param string $uri + * @return Aspamia_Http_Request + */ + public function setUri($uri) + { + // Validate the URI + $uriValid = false; + if ($uri{0} == '/') { + // Absolute URL + // TODO: Validate URL + $uriValid = true; + + } elseif (strpos($uri, 'http') === 0) { + // Absolute Path + // TODO: Validate path + $uriValid = true; + + } elseif ($uri == '*') { + // * - used for OPTIONS method + $uriValid = true; + + } elseif (preg_match('/^[a-zA-Z\-0-9\.]+:\d+$/', $uri)) { + // authority - used for CONNECT method + $uriValid = true; + + } + + if (! $uriValid) { + require_once 'Aspamia/Http/Exception.php'; + throw new Aspamia_Http_Exception("Invalid Request URI: '$uri'"); + } + + $this->_uri = $uri; + return $this; + } + + /** + * Get the request start line - e.g. "GET / HTTP/1.1" + * + * @return string + */ + protected function _getStartLine() + { + return "{$this->_method} {$this->_uri} HTTP/{$this->_httpVersion}"; + } + + /** + * Read an HTTP request from an open socket and return it as an object + * + * Will not read the full body unless explicitly asked to. + * + * @param resource $connection + * @param boolean $read_body + * @return Aspamia_Http_Request + */ + static public function read($connection, $read_body = false) + { + $headerlines = self::_readHeaders($connection); + if (empty($headerlines)) { + throw new ErrorException("Unable to read request: headers are empty"); + } + + $requestline = explode(' ', array_shift($headerlines), 3); + if (! count($requestline) == 3) { + throw new ErrorException("Unable to read request: invalid HTTP request line '{$headerlines[0]}'"); + } + + $protocol = explode('/', $requestline[2]); + if (! ($protocol[0] == 'HTTP' && ($protocol[1] == '1.1' || $protocol[1] == '1.0'))) { + throw new ErrorException("Unsupported protocol version: {$requestline[2]}"); + } + + $method = strtoupper($requestline[0]); + $uri = $requestline[1]; + + $headers = array(); + foreach ($headerlines as $line) { + $header = explode(":", $line, 2); + if (! count($header) == 2) { + throw new ErrorException("Invalid HTTP header format: $line"); + } + + $headers[strtolower(trim($header[0]))] = trim($header[1]); + } + + $request = new Aspamia_Http_Request($method, $uri, $headers); + $request->_httpVersion = $protocol[1]; + $request->_socket = $connection; + + if ($read_body) { + $request->_readBody(); + } + + return $request; + } + + /** + * Read the entire headers section of the response, returning it as an + * array of lines + * + * @param resource $connection + * @return array + */ + static protected function _readHeaders($connection) + { + $headers = array(); + while (($line = @fgets($connection)) !== false) { + $line = trim($line); + if (! $line) break; + $headers[] = $line; + } + + return $headers; + } +} diff --git a/library/Aspamia/Http/Response.php b/library/Aspamia/Http/Response.php new file mode 100644 index 0000000..cec7b8e --- /dev/null +++ b/library/Aspamia/Http/Response.php @@ -0,0 +1,417 @@ + 'Continue', + 101 => 'Switching Protocols', + + // Success 2xx + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + + // Redirection 3xx + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', // 1.1 + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + // 306 is deprecated but reserved + 307 => 'Temporary Redirect', + + // Client Error 4xx + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + + // Server Error 5xx + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 509 => 'Bandwidth Limit Exceeded' + ); + + /** + * The HTTP response code + * + * @var int + */ + protected $_code; + + /** + * The HTTP response code as string + * (e.g. 'Not Found' for 404 or 'Internal Server Error' for 500) + * + * @var string + */ + protected $_message; + + /** + * HTTP response constructor + * + * In most cases, you would use Aspamia_Http_Response::fromString to parse an HTTP + * response string and create a new Aspamia_Http_Response object. + * + * NOTE: The constructor no longer accepts nulls or empty values for the code and + * headers and will throw an exception if the passed values do not form a valid HTTP + * responses. + * + * If no message is passed, the message will be guessed according to the response code. + * + * @param integer $code Response code (200, 404, ...) + * @param array $headers Headers array + * @param string $body Response body + * @param string $version HTTP version + * @param string $message Response code as text + */ + public function __construct($code, array $headers, $body = null, $version = '1.1', $message = null) + { + $this->setCode = $code; + $this->setHeader($headers); + $this->setBody($body); + $this->setHttpVersion($version); + + // If we got the response message, set it. Else, set it according to + // the response code + if (is_string($message)) { + $this->_message = $message; + } else { + $this->_message = self::getHttpReasonPhrase($code); + } + } + + /** + * Get the HTTP response status code + * + * @return int + */ + public function getStatus() + { + return $this->_code; + } + + /** + * Return a message describing the HTTP response code + * (Eg. "OK", "Not Found", "Moved Permanently") + * + * @return string + */ + public function getMessage() + { + return $this->_message; + } + + /** + * Get the request start line - e.g. "GET / HTTP/1.1" + * + * @return string + */ + protected function _getStartLine() + { + return "HTTP/{$this->_httpVersion} {$this->_code} {$this->_message}"; + } + + /** + * Get the standard HTTP 1.1 reason phrase for a status code + * + * Conforms to HTTP/1.1 as defined in RFC 2616 (except for 'Unknown') + * See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 for reference + * + * @param integer $code HTTP statis code + * @return string | null + */ + public static function getHttpReasonPhrase($code) + { + if (isset(self::$_messages[$code])) { + return self::$_messages[$code]; + } else { + return null; + } + } + +// /** +// * Extract the response code from a response string +// * +// * @param string $response_str +// * @return int +// */ +// public static function extractCode($response_str) +// { +// preg_match("|^HTTP/[\d\.x]+ (\d+)|", $response_str, $m); +// +// if (isset($m[1])) { +// return (int) $m[1]; +// } else { +// return false; +// } +// } +// +// /** +// * Extract the HTTP message from a response +// * +// * @param string $response_str +// * @return string +// */ +// public static function extractMessage($response_str) +// { +// preg_match("|^HTTP/[\d\.x]+ \d+ ([^\r\n]+)|", $response_str, $m); +// +// if (isset($m[1])) { +// return $m[1]; +// } else { +// return false; +// } +// } +// +// /** +// * Extract the HTTP version from a response +// * +// * @param string $response_str +// * @return string +// */ +// public static function extractVersion($response_str) +// { +// preg_match("|^HTTP/([\d\.x]+) \d+|", $response_str, $m); +// +// if (isset($m[1])) { +// return $m[1]; +// } else { +// return false; +// } +// } +// +// /** +// * Extract the headers from a response string +// * +// * @param string $response_str +// * @return array +// */ +// public static function extractHeaders($response_str) +// { +// $headers = array(); +// +// // First, split body and headers +// $parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2); +// if (! $parts[0]) return $headers; +// +// // Split headers part to lines +// $lines = explode("\n", $parts[0]); +// unset($parts); +// $last_header = null; +// +// foreach($lines as $line) { +// $line = trim($line, "\r\n"); +// if ($line == "") break; +// +// if (preg_match("|^([\w-]+):\s+(.+)|", $line, $m)) { +// unset($last_header); +// $h_name = strtolower($m[1]); +// $h_value = $m[2]; +// +// if (isset($headers[$h_name])) { +// if (! is_array($headers[$h_name])) { +// $headers[$h_name] = array($headers[$h_name]); +// } +// +// $headers[$h_name][] = $h_value; +// } else { +// $headers[$h_name] = $h_value; +// } +// $last_header = $h_name; +// } elseif (preg_match("|^\s+(.+)$|", $line, $m) && $last_header !== null) { +// if (is_array($headers[$last_header])) { +// end($headers[$last_header]); +// $last_header_key = key($headers[$last_header]); +// $headers[$last_header][$last_header_key] .= $m[1]; +// } else { +// $headers[$last_header] .= $m[1]; +// } +// } +// } +// +// return $headers; +// } +// +// /** +// * Extract the body from a response string +// * +// * @param string $response_str +// * @return string +// */ +// public static function extractBody($response_str) +// { +// $parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2); +// if (isset($parts[1])) { +// return $parts[1]; +// } +// return ''; +// } +// +// /** +// * Decode a "chunked" transfer-encoded body and return the decoded text +// * +// * @param string $body +// * @return string +// */ +// public static function decodeChunkedBody($body) +// { +// $decBody = ''; +// +// while (trim($body)) { +// if (! preg_match("/^([\da-fA-F]+)[^\r\n]*\r\n/sm", $body, $m)) { +// require_once 'Aspamia/Http/Exception.php'; +// throw new Aspamia_Http_Exception("Error parsing _body - doesn't seem to be a chunked _message"); +// } +// +// $length = hexdec(trim($m[1])); +// $cut = strlen($m[0]); +// +// $decBody .= substr($body, $cut, $length); +// $body = substr($body, $cut + $length + 2); +// } +// +// return $decBody; +// } +// +// /** +// * Decode a gzip encoded message (when Content-encoding = gzip) +// * +// * Currently requires PHP with zlib support +// * +// * @param string $body +// * @return string +// */ +// public static function decodeGzip($body) +// { +// if (! function_exists('gzinflate')) { +// require_once 'Aspamia/Http/Exception.php'; +// throw new Aspamia_Http_Exception('Unable to decode gzipped response ' . +// '_body: perhaps the zlib extension is not loaded?'); +// } +// +// return gzinflate(substr($body, 10)); +// } +// +// /** +// * Decode a zlib deflated message (when Content-encoding = deflate) +// * +// * Currently requires PHP with zlib support +// * +// * @param string $body +// * @return string +// */ +// public static function decodeDeflate($body) +// { +// if (! function_exists('gzuncompress')) { +// require_once 'Aspamia/Http/Exception.php'; +// throw new Aspamia_Http_Exception('Unable to decode deflated response ' . +// '_body: perhaps the zlib extension is not loaded?'); +// } +// +// return gzuncompress($body); +// } +// +// /** +// * Create a new Aspamia_Http_Response object from a string +// * +// * @param string $response_str +// * @return Aspamia_Http_Response +// */ +// public static function fromString($response_str) +// { +// $code = self::extractCode($response_str); +// $headers = self::extractHeaders($response_str); +// $body = self::extractBody($response_str); +// $version = self::extractVersion($response_str); +// $message = self::extractMessage($response_str); +// +// return new Aspamia_Http_Response($code, $headers, $body, $version, $message); +// } + +// /** +// * Check whether the response is an error +// * +// * @return boolean +// */ +// public function isError() +// { +// $restype = floor($this->_code / 100); +// if ($restype == 4 || $restype == 5) { +// return true; +// } +// +// return false; +// } +// +// /** +// * Check whether the response in successful +// * +// * @return boolean +// */ +// public function isSuccessful() +// { +// $restype = floor($this->_code / 100); +// if ($restype == 2 || $restype == 1) { // Shouldn't 3xx count as success as well ??? +// return true; +// } +// +// return false; +// } +// +// /** +// * Check whether the response is a redirection +// * +// * @return boolean +// */ +// public function isRedirect() +// { +// $restype = floor($this->_code / 100); +// if ($restype == 3) { +// return true; +// } +// +// return false; +// } +} diff --git a/library/Aspamia/Http/Server.php b/library/Aspamia/Http/Server.php new file mode 100644 index 0000000..015f2a1 --- /dev/null +++ b/library/Aspamia/Http/Server.php @@ -0,0 +1,117 @@ + self::DEFAULT_ADDR, + 'bind_port' => self::DEFAULT_PORT, + 'stream_wrapper' => 'tcp', + 'handler' => 'Aspamia_Http_Server_Handler_Mock' + ); + + protected $_socket = null; + + protected $_context = null; + + /** + * Request handler object + * + * @var Aspamia_Http_Server_Handler_Abstract + */ + protected $_handler = null; + + public function __construct($config = array()) + { + $this->setConfig($config); + + // Initialize handler + if ($this->_config['handler'] instanceof Aspamia_Http_Server_Handler_Abstract) { + $this->_handler = $this->_config['handler']; + + } elseif (is_string($this->_config['handler'])) { + require_once 'Zend/Loader.php'; + Zend_Loader::loadClass($this->_config['handler']); + $handler = new $this->_config['handler']; + + if (! $handler instanceof Aspamia_Http_Server_Handler_Abstract) { + require_once 'Aspamia/Http/Server/Exception.php'; + throw new Aspamia_Http_Server_Exception("Provded handler is not a Aspamia_Http_Server_Handler_Abstract object"); + } + + $this->_handler = $handler; + } + } + + public function setConfig($config) + { + if ($config instanceof Aspamia_Config) { + $config = $config->toArray(); + } + + if (! is_array($config)) { + throw new ErrorException("\$config is expected to be an array or a Aspamia_Config object, got " . gettype($config)); + } + + foreach($config as $k => $v) { + $this->_config[$k] = $v; + } + } + + /** + * TODO: Should this be adapter based? + * + */ + public function run() + { + $addr = $this->_config['stream_wrapper'] . '://' . + $this->_config['bind_addr'] . ':' . + $this->_config['bind_port']; + + $flags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN; + + if (! $this->_context) { + $this->_context = stream_context_create(); + } + + $this->_socket = stream_socket_server($addr, $errno, $errstr, $flags, $this->_context); + if (! $this->_socket) { + return false; + } + + while(true) { + if (($conn = @stream_socket_accept($this->_socket))) { + $this->_handle($conn); + } else { + echo "x\n"; + } + } + + fclose($this->_socket); + } + + public function setHandler(Aspamia_Http_Server_Handler_Abstract $handler) + { + $this->_handler = $handler; + } + + protected function _handle($connection) + { + // Read and parse the HTTP request line + $request = $this->_readRequest($connection); + $response = $this->_handler->handle($request); + + fwrite($connection, (string) $response); + } + + protected function _readRequest($connection) + { + return Aspamia_Http_Request::read($connection); + } +} diff --git a/library/Aspamia/Http/Server/Exception.php b/library/Aspamia/Http/Server/Exception.php new file mode 100644 index 0000000..13f0beb --- /dev/null +++ b/library/Aspamia/Http/Server/Exception.php @@ -0,0 +1,8 @@ +setConfig($config); + } + + public function setConfig($config) + { + if ($config instanceof Aspamia_Config) { + $config = $config->toArray(); + } + + if (! is_array($config)) { + require_once 'Aspamia/Http/Server/Handler/Exception.php'; + throw new Aspamia_Http_Server_Handler_Exception("Configuration is expected to be an array or a Aspamia_Config object, got " . gettype($config)); + } + + foreach ($config as $k => $v) { + $this->_config[$k] = $v; + } + $this->_config = array(); + } + + /** + * Handle the request, return a response + * + * @param Aspamia_Http_Request $request + * @return Aspamia_Http_Response + */ + abstract public function handle(Aspamia_Http_Request $request); +} \ No newline at end of file diff --git a/library/Aspamia/Http/Server/Handler/Cgi.php b/library/Aspamia/Http/Server/Handler/Cgi.php new file mode 100644 index 0000000..15c5adc --- /dev/null +++ b/library/Aspamia/Http/Server/Handler/Cgi.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/library/Aspamia/Http/Server/Handler/Exception.php b/library/Aspamia/Http/Server/Handler/Exception.php new file mode 100644 index 0000000..fffee38 --- /dev/null +++ b/library/Aspamia/Http/Server/Handler/Exception.php @@ -0,0 +1,8 @@ +_response) { + return $this->_response; + } else { + $body = $request->getUri() . "\r\n"; + + return new Aspamia_Http_Response( + 200, + array( + 'X-Powered-By' => 'Aspamia_Http_Server/MockHandler', + 'Content-type' => 'text/plain', + 'Content-length' => strlen($body), + ), + $body + ); + } + } +} \ No newline at end of file diff --git a/library/Aspamia/Http/Server/Handler/Static.php b/library/Aspamia/Http/Server/Handler/Static.php new file mode 100644 index 0000000..08ca227 --- /dev/null +++ b/library/Aspamia/Http/Server/Handler/Static.php @@ -0,0 +1,69 @@ + null, + 'list_directories' => false, + 'followsymlinks' => false, + ); + + public function hanle(Aspamia_Http_Request $request) + { + $oldcwd = getcwd(); + if ($this->_config['document_root']) { + $document_root = $this->_config['document_root']; + chdir($document_root); + } + + $file = ltrim($request->getUri(), '/'); + + if (file_exists($file)) { + if (is_readable($file)) { + if (! $this->_config['followsymlinks'] || ! is_link($file)) { + if (is_file($file)) { + $response = $this->_serveFile($file); + } elseif (is_dir($file) && $this->_config['list_directories']) { + $response = $this->_serveDirListing($file); + } else { + // Directory and can't list, or something else - 403 + $response = new Aspamia_Http_Response(403, array(), "Directory listing not allowed"); + } + } else { + // symlink and we can't follow - 404 + $response = new Aspamia_Http_Response(404, array(), "The requested URL does not exist"); + } + } else { + // not readable - 403 + $response = new Aspamia_Http_Response(403, array(), "You have no permissions to read the requested URL"); + } + } else { + // not found - 404 + $response = new Aspamia_Http_Response(404, "The requested URL does not exist"); + } + + return $response; + } + + protected function _serveFile($file) + { + $size = filesize($file); + $type = $this->_getFileType($file); + + $response = new Aspamia_Http_Response( + 200, + array( + 'Content-type' => $type, + 'Content-length' => $size + ), + file_get_contents($file) + ); + } + + protected function _getFileType($file) + { + return 'text/plain'; + } +} \ No newline at end of file diff --git a/library/Zend/Exception.php b/library/Zend/Exception.php new file mode 100644 index 0000000..599d8a0 --- /dev/null +++ b/library/Zend/Exception.php @@ -0,0 +1,30 @@ + $dir) { + if ($dir == '.') { + $dirs[$key] = $dirPath; + } else { + $dir = rtrim($dir, '\\/'); + $dirs[$key] = $dir . DIRECTORY_SEPARATOR . $dirPath; + } + } + $file = basename($file); + self::loadFile($file, $dirs, true); + } else { + self::_securityCheck($file); + include $file; + } + + if (!class_exists($class, false) && !interface_exists($class, false)) { + require_once 'Zend/Exception.php'; + throw new Zend_Exception("File \"$file\" does not exist or class \"$class\" was not found in the file"); + } + } + + /** + * Loads a PHP file. This is a wrapper for PHP's include() function. + * + * $filename must be the complete filename, including any + * extension such as ".php". Note that a security check is performed that + * does not permit extended characters in the filename. This method is + * intended for loading Zend Framework files. + * + * If $dirs is a string or an array, it will search the directories + * in the order supplied, and attempt to load the first matching file. + * + * If the file was not found in the $dirs, or if no $dirs were specified, + * it will attempt to load it from PHP's include_path. + * + * If $once is TRUE, it will use include_once() instead of include(). + * + * @param string $filename + * @param string|array $dirs - OPTIONAL either a path or array of paths + * to search. + * @param boolean $once + * @return boolean + * @throws Zend_Exception + */ + public static function loadFile($filename, $dirs = null, $once = false) + { + self::_securityCheck($filename); + + /** + * Search in provided directories, as well as include_path + */ + $incPath = false; + if (!empty($dirs) && (is_array($dirs) || is_string($dirs))) { + if (is_array($dirs)) { + $dirs = implode(PATH_SEPARATOR, $dirs); + } + $incPath = get_include_path(); + set_include_path($dirs . PATH_SEPARATOR . $incPath); + } + + /** + * Try finding for the plain filename in the include_path. + */ + if ($once) { + include_once $filename; + } else { + include $filename; + } + + /** + * If searching in directories, reset include_path + */ + if ($incPath) { + set_include_path($incPath); + } + + return true; + } + + /** + * Returns TRUE if the $filename is readable, or FALSE otherwise. + * This function uses the PHP include_path, where PHP's is_readable() + * does not. + * + * Note from ZF-2900: + * If you use custom error handler, please check whether return value + * from error_reporting() is zero or not. + * At mark of fopen() can not suppress warning if the handler is used. + * + * @param string $filename + * @return boolean + */ + public static function isReadable($filename) + { + if (!$fh = @fopen($filename, 'r', true)) { + return false; + } + @fclose($fh); + return true; + } + + /** + * spl_autoload() suitable implementation for supporting class autoloading. + * + * Attach to spl_autoload() using the following: + * + * spl_autoload_register(array('Zend_Loader', 'autoload')); + * + * + * @param string $class + * @return string|false Class name on success; false on failure + */ + public static function autoload($class) + { + try { + @self::loadClass($class); + return $class; + } catch (Exception $e) { + return false; + } + } + + /** + * Register {@link autoload()} with spl_autoload() + * + * @param string $class (optional) + * @param boolean $enabled (optional) + * @return void + * @throws Zend_Exception if spl_autoload() is not found + * or if the specified class does not have an autoload() method. + */ + public static function registerAutoload($class = 'Zend_Loader', $enabled = true) + { + if (!function_exists('spl_autoload_register')) { + require_once 'Zend/Exception.php'; + throw new Zend_Exception('spl_autoload does not exist in this PHP installation'); + } + + self::loadClass($class); + $methods = get_class_methods($class); + if (!in_array('autoload', (array) $methods)) { + require_once 'Zend/Exception.php'; + throw new Zend_Exception("The class \"$class\" does not have an autoload() method"); + } + + if ($enabled === true) { + spl_autoload_register(array($class, 'autoload')); + } else { + spl_autoload_unregister(array($class, 'autoload')); + } + } + + /** + * Ensure that filename does not contain exploits + * + * @param string $filename + * @return void + * @throws Zend_Exception + */ + protected static function _securityCheck($filename) + { + /** + * Security check + */ + if (preg_match('/[^a-z0-9\\/\\\\_.:-]/i', $filename)) { + require_once 'Zend/Exception.php'; + throw new Zend_Exception('Security check: Illegal character in filename'); + } + } + + /** + * Attempt to include() the file. + * + * include() is not prefixed with the @ operator because if + * the file is loaded and contains a parse error, execution + * will halt silently and this is difficult to debug. + * + * Always set display_errors = Off on production servers! + * + * @param string $filespec + * @param boolean $once + * @return boolean + * @deprecated Since 1.5.0; use loadFile() instead + */ + protected static function _includeFile($filespec, $once = false) + { + if ($once) { + return include_once $filespec; + } else { + return include $filespec ; + } + } +}