diff -r c2f4c900c507 -r dc838fd61a06 includes/http.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/includes/http.php Thu Dec 20 22:23:07 2007 -0500 @@ -0,0 +1,792 @@ +method == GET. + * @var array (associative) + */ + + var $parms_post = array(); + + /** + * The list of cookies that will be sent. + * @var array (associative) + */ + + var $cookies_out = array(); + + /** + * Additional request headers. + * @var array (associative) + */ + + var $headers = array(); + + /** + * Cached response. + * @var string, or bool:false if the request hasn't been sent yet + */ + + var $response = false; + + /** + * Cached response code + * @var int set to -1 if request hasn't been sent yet + */ + + var $response_code = -1; + + /** + * Cached response code string + * @var string or bool:false if the request hasn't been sent yet + */ + + var $response_string = false; + + /** + * Resource for the socket. False if a connection currently isn't going. + * @var resource + */ + + var $socket = false; + + /** + * The state of our request. 0 means it hasn't been made yet. 1 means the socket is open, 2 means the socket is open and the request has been written, 3 means the headers have been fetched, and 4 means the request is completed. + * @var int + */ + + var $state = 0; + + /** + * Constructor. + * @param string Hostname to send to + * @param string URI (/index.php) + * @param string Request method - GET or POST. + * @param int Optional. The port to open the request on. Defaults to 80. + */ + + function Request_HTTP($host, $uri, $method = 'GET', $port = 80) + { + if ( !preg_match('/^(([a-z0-9-]+\.)*?)([a-z0-9-]+)$/', $host) ) + die(__CLASS__ . ': Invalid hostname'); + $this->host = $host; + $this->uri = $uri; + if ( is_int($port) && $port >= 1 && $port <= 65535 ) + $this->port = $port; + else + die(__CLASS__ . ': Invalid port'); + $method = strtoupper($method); + if ( $method == 'GET' || $method == 'POST' ) + $this->method = $method; + else + die(__CLASS__ . ': Invalid request method'); + + $newline = "\r\n"; + $php_ver = PHP_VERSION; + $this->add_header('User-Agent', "PHP/$php_ver (Server: {$_SERVER['SERVER_SOFTWARE']}; automated bot request)"); + } + + /** + * Sets one or more cookies to be sent to the server. + * @param string or array If a string, the cookie name. If an array, associative array in the form of cookiename => cookievalue + * @param string or bool If a string, the cookie value. If boolean, defaults to false, param 1 should be an array, and this should not be passed. + */ + + function add_cookie($cookiename, $cookievalue = false) + { + if ( is_array($cookiename) && !$cookievalue ) + { + foreach ( $cookiename as $name => $value ) + { + $this->cookies_out[$name] = $value; + } + } + else if ( is_string($cookiename) && is_string($cookievalue) ) + { + $this->cookies_out[$cookiename] = $cookievalue; + } + else + { + die(__CLASS__ . '::' . __METHOD__ . ': Invalid argument(s)'); + } + } + + /** + * Sets one or more request header values. + * @param string or array If a string, the header name. If an array, associative array in the form of headername => headervalue + * @param string or bool If a string, the header value. If boolean, defaults to false, param 1 should be an array, and this should not be passed. + */ + + function add_header($headername, $headervalue = false) + { + if ( is_array($headername) && !$headervalue ) + { + foreach ( $headername as $name => $value ) + { + $this->headers[$name] = $value; + } + } + else if ( is_string($headername) && is_string($headervalue) ) + { + $this->headers[$headername] = $headervalue; + } + else + { + die(__CLASS__ . '::' . __METHOD__ . ': Invalid argument(s)'); + } + } + + /** + * Adds one or more values to be passed on GET. + * @param string or array If a string, the parameter name. If an array, associative array in the form of parametername => parametervalue + * @param string or bool If a string, the parameter value. If boolean, defaults to false, param 1 should be an array, and this should not be passed. + */ + + function add_get($getname, $getvalue = false) + { + if ( is_array($getname) && !$getvalue ) + { + foreach ( $getname as $name => $value ) + { + $this->parms_get[$name] = $value; + } + } + else if ( is_string($getname) && is_string($getvalue) ) + { + $this->parms_get[$getname] = $getvalue; + } + else + { + die(__CLASS__ . '::' . __METHOD__ . ': Invalid argument(s)'); + } + } + + /** + * Adds one or more values to be passed on POST. + * @param string or array If a string, the header name. If an array, associative array in the form of headername => headervalue + * @param string or bool If a string, the header value. If boolean, defaults to false, param 1 should be an array, and this should not be passed. + */ + + function add_post($postname, $postvalue = false) + { + if ( is_array($postname) && !$postvalue ) + { + foreach ( $postname as $name => $value ) + { + $this->parms_post[$name] = $value; + } + } + else if ( is_string($postname) && is_string($postvalue) ) + { + $this->parms_post[$postname] = $postvalue; + } + else + { + die(__CLASS__ . '::' . __METHOD__ . ': Invalid argument(s)'); + } + } + + /** + * Internal function to open up the socket. + * @access private + */ + + function _sock_open(&$connection) + { + if ( $this->debug ) + { + echo '
'; + echo '

' . __CLASS__ . ': Sending request

Request parameters:

'; + echo "

Headers:

$headers
"; + echo "

Cookies: $cookies

"; + echo "

GET URI: " . htmlspecialchars($get) . "

"; + echo "

POST DATA: " . htmlspecialchars($post) . "

"; + } + + // Open connection + $connection = fsockopen($this->host, $this->port); + if ( !$connection ) + die(__CLASS__ . '::' . __METHOD__ . ': Could not make connection'); + + if ( $this->debug ) + echo '

Connection opened. Writing main request to socket. Raw socket data follows.

';
+    
+    // 1 = socket open
+    $this->state = 1;
+  }
+  
+  /**
+   * Internal function to actually write the request into the socket.
+   * @access private
+   */
+  
+  function _write_request(&$connection, &$headers, &$cookies, &$get, &$post)
+  {
+    $newline = "\r\n";
+    
+    $this->_fputs($connection, "{$this->method} {$this->uri}{$get} HTTP/1.1{$newline}");
+    $this->_fputs($connection, "Host: {$this->host}{$newline}");
+    $this->_fputs($connection, $headers);
+    $this->_fputs($connection, $cookies);
+    
+    if ( $this->method == 'POST' )
+    {
+      // POST-specific parameters
+      $post_length = strlen($post);
+      $this->_fputs($connection, "Content-type: application/x-www-form-urlencoded{$newline}");
+      $this->_fputs($connection, "Content-length: {$post_length}{$newline}");
+    }
+    
+    $this->_fputs($connection, "Connection: close{$newline}");
+    $this->_fputs($connection, "{$newline}");
+    
+    if ( $this->method == 'POST' )
+    {
+      $this->_fputs($connection, $post);
+    }
+    
+    if ( $this->debug )
+      echo '

Request written. Fetching response.

'; + + // 2 = request written + $this->state = 2; + } + + /** + * Wrap up and close the socket. Nothing more than a call to fsockclose() except in debug mode. + * @access private + */ + + function sock_close(&$connection) + { + if ( $this->debug ) + { + echo '

Response fetched. Closing connection. Response text follows.

';
+      echo htmlspecialchars($buffer);
+      echo '

'; + } + + fclose($connection); + } + + /** + * Internal function to grab the response code and status string + * @access string + */ + + function _parse_response_code($buffer) + { + // Retrieve response code and status + $pos_newline = strpos($buffer, "\n"); + $pos_carriage_return = strpos($buffer, "\r"); + $pos_end_first_line = ( $pos_carriage_return < $pos_newline && $pos_carriage_return > 0 ) ? $pos_carriage_return : $pos_newline; + + // First line is in format of: + // HTTP/1.1 ### Blah blah blah(\r?)\n + $response_code = substr($buffer, 9, 3); + $response_string = substr($buffer, 13, ( $pos_end_first_line - 13 ) ); + $this->response_code = intval($response_code); + $this->response_string = $response_string; + } + + /** + * Internal function to send the request. + * @access private + */ + + function _send_request() + { + $this->concat_headers($headers, $cookies, $get, $post); + + if ( $this->state < 1 ) + { + $this->_sock_open($this->socket); + } + if ( $this->state < 2 ) + { + $this->_write_request($this->socket, $headers, $cookies, $get, $post); + } + if ( $this->state == 2 ) + { + $buffer = $this->_read_until_newlines($this->socket); + $this->state = 3; + $this->_parse_response_code($buffer); + $this->response = $buffer; + } + if ( $this->state == 3 ) + { + // Determine transfer encoding + $is_chunked = preg_match("/Transfer-Encoding: (chunked)\r?\n/", $this->response); + + $buffer = ''; + while ( !feof($this->socket) ) + { + $part = fgets($this->socket, 1024); + if ( $is_chunked && preg_match("/^([a-f0-9]+)\x0D\x0A$/", $part, $match) ) + { + $chunklen = hexdec($match[1]); + $part = ( $chunklen > 0 ) ? fread($this->socket, $chunklen) : ''; + // remove the last newline from $part + $part = preg_replace("/\r?\n\$/m", "", $part); + } + $buffer .= $part; + } + $this->response .= $buffer; + } + $this->state = 4; + + $this->sock_close($this->socket); + $this->socket = false; + } + + /** + * Internal function to send the request but only fetch the headers. Leaves a connection open for a finish-up function. + * @access private + */ + + function _send_request_headers_only() + { + $this->concat_headers($headers, $cookies, $get, $post); + + if ( $this->state < 1 ) + { + $this->_sock_open($this->socket); + } + if ( $this->state < 2 ) + { + $this->_write_request($this->socket, $headers, $cookies, $get, $post); + } + if ( $this->state == 2 ) + { + $buffer = $this->_read_until_newlines($this->socket); + $this->state = 3; + $this->_parse_response_code($buffer); + $this->response = $buffer; + } + } + + /** + * Internal function to read from a socket until two consecutive newlines are hit. + * @access private + */ + + function _read_until_newlines($sock) + { + $prev_char = ''; + $prev1_char = ''; + $prev2_char = ''; + $buf = ''; + while ( !feof($sock) ) + { + $chr = fread($sock, 1); + $buf .= $chr; + if ( ( $chr == "\n" && $prev_char == "\n" ) || + ( $chr == "\n" && $prev_char == "\r" && $prev1_char == "\n" && $prev2_char == "\r" ) ) + { + return $buf; + } + $prev2_char = $prev1_char; + $prev1_char = $prev_char; + $prev_char = $chr; + } + return $buf; + } + + /** + * Returns the response text. If the request hasn't been sent, it will be sent here. + * @return string + */ + + function get_response() + { + if ( $this->state == 4 ) + return $this->response; + $this->_send_request(); + return $this->response; + } + + /** + * Writes the response body to a file. This is good for conserving memory when downloading large files. If the file already exists it will be overwritten. + * @param string File to write to + * @param int Chunk size in KB to read from the socket. Optional and should only be needed in circumstances when extreme memory conservation is needed. Defaults to 768. + * @param int Maximum file size. Defaults to 0, which means no limit. + * @return bool True on success, false on failure + */ + + function write_response_to_file($file, $chunklen = 768, $max_file_size = 0) + { + if ( !is_writeable( dirname($file) ) || !file_exists( dirname($file) ) ) + { + return false; + } + $handle = @fopen($file, 'w'); + if ( !$handle ) + return false; + $chunklen = intval($chunklen); + if ( $chunklen < 1 ) + return false; + if ( $this->state == 4 ) + { + // we already have the response, so cheat + $response = $this->get_response_body(); + fwrite($handle, $response); + } + else + { + // read data from the socket, write it immediately, and unset to free memory + $headers = $this->get_response_headers(); + $transferred_bytes = 0; + $bandwidth_exceeded = false; + // if transfer-encoding is chunked, read using chunk sizes the server specifies + $is_chunked = preg_match("/Transfer-Encoding: (chunked)\r?\n/", $this->response); + if ( $is_chunked ) + { + $buffer = ''; + while ( !feof($this->socket) ) + { + $part = fgets($this->socket, ( 1024 * $chunklen )); + // Theoretically if the encoding is really chunked then this should always match. + if ( $is_chunked && preg_match("/^([a-f0-9]+)\x0D\x0A$/", $part, $match) ) + { + $chunk_length = hexdec($match[1]); + $part = ( $chunk_length > 0 ) ? fread($this->socket, $chunk_length) : ''; + // remove the last newline from $part + $part = preg_replace("/\r?\n\$/m", "", $part); + } + + $transferred_bytes += strlen($part); + if ( $max_file_size && $transferred_bytes > $max_file_size ) + { + // truncate output to $max_file_size bytes + $partlen = $max_file_size - ( $transferred_bytes - strlen($part) ); + $part = substr($part, 0, $partlen); + $bandwidth_exceeded = true; + } + fwrite($handle, $part); + if ( $bandwidth_exceeded ) + { + break; + } + } + } + else + { + $first_chunk = fread($this->socket, ( 1024 * $chunklen )); + fwrite($handle, $first_chunk); + while ( !feof($this->socket) ) + { + $chunk = fread($this->socket, ( 1024 * $chunklen )); + + $transferred_bytes += strlen($chunk); + if ( $max_file_size && $transferred_bytes > $max_file_size ) + { + // truncate output to $max_file_size bytes + $partlen = $max_file_size - ( $transferred_bytes - strlen($chunk) ); + $chunk = substr($chunk, 0, $partlen); + $bandwidth_exceeded = true; + } + + fwrite($handle, $chunk); + unset($chunk); + + if ( $bandwidth_exceeded ) + { + break; + } + } + } + } + fclose($handle); + // close socket and reset state, since we haven't cached the response + $this->sock_close($this->socket); + $this->state = 0; + return ($bandwidth_exceeded) ? false : true; + } + + /** + * Returns only the response headers. + * @return string + */ + + function get_response_headers() + { + if ( $this->state == 3 ) + { + return $this->response; + } + else if ( $this->state == 4 ) + { + $pos_end = strpos($this->response, "\r\n\r\n"); + $data = substr($this->response, 0, $pos_start); + return $data; + } + else + { + $this->_send_request_headers_only(); + return $this->response; + } + } + + /** + * Returns only the response headers, as an associative array. + * @return array + */ + + function get_response_headers_array() + { + $data = $this->get_response_headers(); + preg_match_all("/(^|\n)([A-z0-9_-]+?): (.+?)(\r|\n|\$)/", $data, $matches); + $headers = array(); + for ( $i = 0; $i < count($matches[0]); $i++ ) + { + $headers[ $matches[2][$i] ] = $matches[3][$i]; + } + return $headers; + } + + /** + * Returns only the body (not the headers) of the response. If the request hasn't been sent, it will be sent here. + * @return string + */ + + function get_response_body() + { + $data = $this->get_response(); + $pos_start = strpos($data, "\r\n\r\n") + 4; + $data = substr($data, $pos_start); + return $data; + } + + /** + * Returns all cookies requested to be set by the server as an associative array. If the request hasn't been sent, it will be sent here. + * @return array + */ + + function get_cookies() + { + $data = $this->get_response(); + $data = str_replace("\r\n", "\n", $data); + $pos = strpos($data, "\n\n"); + $headers = substr($data, 0, $pos); + preg_match_all("/Set-Cookie: ([a-z0-9_]+)=([^;]+);( expires=([^;]+);)?( path=(.*?))?\n/", $headers, $cookiematch); + if ( count($cookiematch[0]) < 1 ) + return array(); + $cookies = array(); + foreach ( $cookiematch[0] as $i => $cookie ) + { + $cookies[$cookiematch[1][$i]] = $cookiematch[2][$i]; + } + return $cookies; + } + + /** + * Internal method to write data to a socket with debugging possibility. + * @access private + */ + + function _fputs($socket, $data) + { + if ( $this->debug ) + echo htmlspecialchars($data); + return fputs($socket, $data); + } + + /** + * Internal function to stringify cookies, headers, get, and post. + * @access private + */ + + function concat_headers(&$headers, &$cookies, &$get, &$post) + { + $headers = ''; + $cookies = ''; + foreach ( $this->headers as $name => $value ) + { + $value = str_replace('\\n', '\\\\n', $value); + $value = str_replace("\n", '\\n', $value); + $headers .= "$name: $value\r\n"; + } + unset($value); + if ( count($this->cookies_out) > 0 ) + { + $i = 0; + $cookie_header = 'Cookie: '; + foreach ( $this->cookies_out as $name => $value ) + { + $i++; + if ( $i > 1 ) + $cookie_header .= '; '; + $value = str_replace(';', rawurlencode(';'), $value); + $value = str_replace('\\n', '\\\\n', $value); + $value = str_replace("\n", '\\n', $value); + $cookie_header .= "$name=$value"; + } + $cookie_header .= "\r\n"; + $cookies = $cookie_header; + unset($value, $cookie_header); + } + if ( count($this->parms_get) > 0 ) + { + $get = '?'; + $i = 0; + foreach ( $this->parms_get as $name => $value ) + { + $i++; + if ( $i > 1 ) + $get .= '&'; + $value = urlencode($value); + if ( !empty($value) ) + $get .= "$name=$value"; + else + $get .= "$name"; + } + } + if ( count($this->parms_post) > 0 ) + { + $post = ''; + $i = 0; + foreach ( $this->parms_post as $name => $value ) + { + $i++; + if ( $i > 1 ) + $post .= '&'; + $value = urlencode($value); + $post .= "$name=$value"; + } + } + } + +} + +?>