body_length += $length; if ($this->body_length > $this->max_body_size) { return -1; } $this->body .= $buffer; return $length; } /** * cURL callback to read HTTP headers * * @access public * @param resource $ch cURL handler * @param string $buffer Header line * @return integer Length of the buffer */ public function readHeaders($ch, $buffer) { $length = strlen($buffer); if ($buffer === "\r\n") { $this->headers_counter++; } else { if (! isset($this->headers[$this->headers_counter])) { $this->headers[$this->headers_counter] = ''; } $this->headers[$this->headers_counter] .= $buffer; } return $length; } /** * cURL callback to passthrough the HTTP status header to the client * * @access public * @param resource $ch cURL handler * @param string $buffer Header line * @return integer Length of the buffer */ public function passthroughHeaders($ch, $buffer) { list($status, $headers) = HttpHeaders::parse(array($buffer)); if ($status !== 0) { header(':', true, $status); } elseif (isset($headers['Content-Type'])) { header($buffer); } return $this->readHeaders($ch, $buffer); } /** * cURL callback to passthrough the HTTP body to the client * * If the function return -1, curl stop to read the HTTP response * * @access public * @param resource $ch cURL handler * @param string $buffer Chunk of data * @return integer Length of the buffer */ public function passthroughBody($ch, $buffer) { echo $buffer; return strlen($buffer); } /** * Prepare HTTP headers * * @access private * @return string[] */ private function prepareHeaders() { $headers = array( 'Connection: close', 'User-Agent: '.$this->user_agent, ); if ($this->etag) { $headers[] = 'If-None-Match: '.$this->etag; } if ($this->last_modified) { $headers[] = 'If-Modified-Since: '.$this->last_modified; } return $headers; } /** * Prepare curl proxy context * * @access private * @return resource $ch */ private function prepareProxyContext($ch) { if ($this->proxy_hostname) { Logger::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port); curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxy_port); curl_setopt($ch, CURLOPT_PROXYTYPE, 'HTTP'); curl_setopt($ch, CURLOPT_PROXY, $this->proxy_hostname); if ($this->proxy_username) { Logger::setMessage(get_called_class().' Proxy credentials: Yes'); curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->proxy_username.':'.$this->proxy_password); } else { Logger::setMessage(get_called_class().' Proxy credentials: No'); } } return $ch; } /** * Prepare curl auth context * * @access private * @return resource $ch */ private function prepareAuthContext($ch) { if ($this->username && $this->password) { curl_setopt($ch, CURLOPT_USERPWD, $this->username.':'.$this->password); } return $ch; } /** * Set write/header functions * * @access private * @return resource $ch */ private function prepareDownloadMode($ch) { $write_function = 'readBody'; $header_function = 'readHeaders'; if ($this->isPassthroughEnabled()) { $write_function = 'passthroughBody'; $header_function = 'passthroughHeaders'; } curl_setopt($ch, CURLOPT_WRITEFUNCTION, array($this, $write_function)); curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, $header_function)); return $ch; } /** * Prepare curl context * * @access private * @return resource */ private function prepareContext() { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $this->url); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_HTTPHEADER, $this->prepareHeaders()); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, ini_get('open_basedir') === ''); curl_setopt($ch, CURLOPT_MAXREDIRS, $this->max_redirects); curl_setopt($ch, CURLOPT_ENCODING, ''); curl_setopt($ch, CURLOPT_COOKIEJAR, 'php://memory'); curl_setopt($ch, CURLOPT_COOKIEFILE, 'php://memory'); curl_setopt($ch, CURLOPT_SSLVERSION, 1); // Enforce TLS v1 $ch = $this->prepareDownloadMode($ch); $ch = $this->prepareProxyContext($ch); $ch = $this->prepareAuthContext($ch); return $ch; } /** * Execute curl context * * @access private */ private function executeContext() { $ch = $this->prepareContext(); curl_exec($ch); Logger::setMessage(get_called_class().' cURL total time: '.curl_getinfo($ch, CURLINFO_TOTAL_TIME)); Logger::setMessage(get_called_class().' cURL dns lookup time: '.curl_getinfo($ch, CURLINFO_NAMELOOKUP_TIME)); Logger::setMessage(get_called_class().' cURL connect time: '.curl_getinfo($ch, CURLINFO_CONNECT_TIME)); Logger::setMessage(get_called_class().' cURL speed download: '.curl_getinfo($ch, CURLINFO_SPEED_DOWNLOAD)); Logger::setMessage(get_called_class().' cURL effective url: '.curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)); $curl_errno = curl_errno($ch); if ($curl_errno) { Logger::setMessage(get_called_class().' cURL error: '.curl_error($ch)); curl_close($ch); $this->handleError($curl_errno); } // Update the url if there where redirects $this->url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); curl_close($ch); } /** * Do the HTTP request * * @access public * @param bool $follow_location Flag used when there is an open_basedir restriction * @return array HTTP response ['body' => ..., 'status' => ..., 'headers' => ...] */ public function doRequest($follow_location = true) { $this->executeContext(); list($status, $headers) = HttpHeaders::parse(explode("\r\n", $this->headers[$this->headers_counter - 1])); // When restricted with open_basedir if ($this->needToHandleRedirection($follow_location, $status)) { return $this->handleRedirection($headers['Location']); } return array( 'status' => $status, 'body' => $this->body, 'headers' => $headers ); } /** * Check if the redirection have to be handled manually * * @access private * @param boolean $follow_location Flag * @param integer $status HTTP status code * @return boolean */ private function needToHandleRedirection($follow_location, $status) { return $follow_location && ini_get('open_basedir') !== '' && ($status == 301 || $status == 302); } /** * Handle manually redirections when there is an open base dir restriction * * @access private * @param string $location Redirected URL * @return array */ private function handleRedirection($location) { $nb_redirects = 0; $result = array(); $this->url = Url::resolve($location, $this->url); $this->body = ''; $this->body_length = 0; $this->headers = array(); $this->headers_counter = 0; while (true) { $nb_redirects++; if ($nb_redirects >= $this->max_redirects) { throw new MaxRedirectException('Maximum number of redirections reached'); } $result = $this->doRequest(false); if ($result['status'] == 301 || $result['status'] == 302) { $this->url = Url::resolve($result['headers']['Location'], $this->url); $this->body = ''; $this->body_length = 0; $this->headers = array(); $this->headers_counter = 0; } else { break; } } return $result; } /** * Handle cURL errors (throw individual exceptions) * * We don't use constants because they are not necessary always available * (depends of the version of libcurl linked to php) * * @see http://curl.haxx.se/libcurl/c/libcurl-errors.html * @access private * @param integer $errno cURL error code */ private function handleError($errno) { switch ($errno) { case 78: // CURLE_REMOTE_FILE_NOT_FOUND throw new InvalidUrlException('Resource not found'); case 6: // CURLE_COULDNT_RESOLVE_HOST throw new InvalidUrlException('Unable to resolve hostname'); case 7: // CURLE_COULDNT_CONNECT throw new InvalidUrlException('Unable to connect to the remote host'); case 23: // CURLE_WRITE_ERROR throw new MaxSizeException('Maximum response size exceeded'); case 28: // CURLE_OPERATION_TIMEDOUT throw new TimeoutException('Operation timeout'); case 35: // CURLE_SSL_CONNECT_ERROR case 51: // CURLE_PEER_FAILED_VERIFICATION case 58: // CURLE_SSL_CERTPROBLEM case 60: // CURLE_SSL_CACERT case 59: // CURLE_SSL_CIPHER case 64: // CURLE_USE_SSL_FAILED case 66: // CURLE_SSL_ENGINE_INITFAILED case 77: // CURLE_SSL_CACERT_BADFILE case 83: // CURLE_SSL_ISSUER_ERROR throw new InvalidCertificateException('Invalid SSL certificate'); case 47: // CURLE_TOO_MANY_REDIRECTS throw new MaxRedirectException('Maximum number of redirections reached'); case 63: // CURLE_FILESIZE_EXCEEDED throw new MaxSizeException('Maximum response size exceeded'); default: throw new InvalidUrlException('Unable to fetch the URL'); } } }