body_length += $length; if ($this->body_length > $this->max_body_size) { return -1; } $this->body .= $buffer; return $length; } /** * cURL callback to read HTTP headers. * * @param resource $ch cURL handler * @param string $buffer Header line * * @return int Length of the buffer */ public function readHeaders($ch, $buffer) { $length = strlen($buffer); if ($buffer === "\r\n" || $buffer === "\n") { ++$this->response_headers_count; } else { if (!isset($this->response_headers[$this->response_headers_count])) { $this->response_headers[$this->response_headers_count] = ''; } $this->response_headers[$this->response_headers_count] .= $buffer; } return $length; } /** * cURL callback to passthrough the HTTP body to the client. * * If the function return -1, curl stop to read the HTTP response * * @param resource $ch cURL handler * @param string $buffer Chunk of data * * @return int Length of the buffer */ public function passthroughBody($ch, $buffer) { // do it only at the beginning of a transmission if ($this->body_length === 0) { list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1])); if ($this->isRedirection($status)) { return $this->handleRedirection($headers['Location']); } // Do not work with PHP-FPM if (strpos(PHP_SAPI, 'cgi') !== false) { header(':', true, $status); } if (isset($headers['Content-Type'])) { header('Content-Type:' .$headers['Content-Type']); } } $length = strlen($buffer); $this->body_length += $length; echo $buffer; return $length; } /** * Prepare HTTP headers. * * @return string[] */ private function prepareHeaders() { $headers = array( 'Connection: close', ); if ($this->etag) { $headers[] = 'If-None-Match: '.$this->etag; $headers[] = 'A-IM: feed'; } if ($this->last_modified) { $headers[] = 'If-Modified-Since: '.$this->last_modified; } $headers = array_merge($headers, $this->request_headers); return $headers; } /** * Prepare curl proxy context. * * @param resource $ch * * @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. * * @param resource $ch * * @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. * * @param resource $ch * * @return resource $ch */ private function prepareDownloadMode($ch) { $this->body = ''; $this->response_headers = array(); $this->response_headers_count = 0; $write_function = 'readBody'; $header_function = 'readHeaders'; if ($this->isPassthroughEnabled()) { $write_function = 'passthroughBody'; } curl_setopt($ch, CURLOPT_WRITEFUNCTION, array($this, $write_function)); curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, $header_function)); return $ch; } /** * Prepare curl context. * * @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_TIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_USERAGENT, $this->user_agent); curl_setopt($ch, CURLOPT_HTTPHEADER, $this->prepareHeaders()); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); curl_setopt($ch, CURLOPT_ENCODING, ''); curl_setopt($ch, CURLOPT_COOKIEJAR, 'php://memory'); curl_setopt($ch, CURLOPT_COOKIEFILE, 'php://memory'); // Disable SSLv3 by enforcing TLSv1.x for curl >= 7.34.0 and < 7.39.0. // Versions prior to 7.34 and at least when compiled against openssl // interpret this parameter as "limit to TLSv1.0" which fails for sites // which enforce TLS 1.1+. // Starting with curl 7.39.0 SSLv3 is disabled by default. $version = curl_version(); if ($version['version_number'] >= 467456 && $version['version_number'] < 468736) { curl_setopt($ch, CURLOPT_SSLVERSION, 1); } $ch = $this->prepareDownloadMode($ch); $ch = $this->prepareProxyContext($ch); $ch = $this->prepareAuthContext($ch); return $ch; } /** * Execute curl context. */ 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. * * @return array HTTP response ['body' => ..., 'status' => ..., 'headers' => ...] */ public function doRequest() { $this->executeContext(); list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1])); if ($this->isRedirection($status)) { if (empty($headers['Location'])) { $status = 200; } else { return $this->handleRedirection($headers['Location']); } } return array( 'status' => $status, 'body' => $this->body, 'headers' => $headers, ); } /** * Handle HTTP redirects * * @param string $location Redirected URL * @return array * @throws MaxRedirectException */ private function handleRedirection($location) { $result = array(); $this->url = Url::resolve($location, $this->url); $this->body = ''; $this->body_length = 0; $this->response_headers = array(); $this->response_headers_count = 0; while (true) { $this->nbRedirects++; if ($this->nbRedirects >= $this->max_redirects) { throw new MaxRedirectException('Maximum number of redirections reached'); } $result = $this->doRequest(); if ($this->isRedirection($result['status'])) { $this->url = Url::resolve($result['headers']['Location'], $this->url); $this->body = ''; $this->body_length = 0; $this->response_headers = array(); $this->response_headers_count = 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 * * @param int $errno cURL error code * @throws InvalidCertificateException * @throws InvalidUrlException * @throws MaxRedirectException * @throws MaxSizeException * @throws TimeoutException */ private function handleError($errno) { switch ($errno) { case 78: // CURLE_REMOTE_FILE_NOT_FOUND throw new InvalidUrlException('Resource not found', $errno); case 6: // CURLE_COULDNT_RESOLVE_HOST throw new InvalidUrlException('Unable to resolve hostname', $errno); case 7: // CURLE_COULDNT_CONNECT throw new InvalidUrlException('Unable to connect to the remote host', $errno); case 23: // CURLE_WRITE_ERROR throw new MaxSizeException('Maximum response size exceeded', $errno); case 28: // CURLE_OPERATION_TIMEDOUT throw new TimeoutException('Operation timeout', $errno); 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 $msg = 'Invalid SSL certificate caused by CURL error number ' . $errno; throw new InvalidCertificateException($msg, $errno); case 47: // CURLE_TOO_MANY_REDIRECTS throw new MaxRedirectException('Maximum number of redirections reached', $errno); case 63: // CURLE_FILESIZE_EXCEEDED throw new MaxSizeException('Maximum response size exceeded', $errno); default: throw new InvalidUrlException('Unable to fetch the URL', $errno); } } }