Curl.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. <?php
  2. namespace PicoFeed\Client;
  3. use PicoFeed\Logging\Logger;
  4. /**
  5. * cURL HTTP client.
  6. *
  7. * @author Frederic Guillot
  8. */
  9. class Curl extends Client
  10. {
  11. protected $nbRedirects = 0;
  12. /**
  13. * HTTP response body.
  14. *
  15. * @var string
  16. */
  17. private $body = '';
  18. /**
  19. * Body size.
  20. *
  21. * @var int
  22. */
  23. private $body_length = 0;
  24. /**
  25. * HTTP response headers.
  26. *
  27. * @var array
  28. */
  29. private $response_headers = array();
  30. /**
  31. * Counter on the number of header received.
  32. *
  33. * @var int
  34. */
  35. private $response_headers_count = 0;
  36. /**
  37. * cURL callback to read the HTTP body.
  38. *
  39. * If the function return -1, curl stop to read the HTTP response
  40. *
  41. * @param resource $ch cURL handler
  42. * @param string $buffer Chunk of data
  43. *
  44. * @return int Length of the buffer
  45. */
  46. public function readBody($ch, $buffer)
  47. {
  48. $length = strlen($buffer);
  49. $this->body_length += $length;
  50. if ($this->body_length > $this->max_body_size) {
  51. return -1;
  52. }
  53. $this->body .= $buffer;
  54. return $length;
  55. }
  56. /**
  57. * cURL callback to read HTTP headers.
  58. *
  59. * @param resource $ch cURL handler
  60. * @param string $buffer Header line
  61. *
  62. * @return int Length of the buffer
  63. */
  64. public function readHeaders($ch, $buffer)
  65. {
  66. $length = strlen($buffer);
  67. if ($buffer === "\r\n" || $buffer === "\n") {
  68. ++$this->response_headers_count;
  69. } else {
  70. if (!isset($this->response_headers[$this->response_headers_count])) {
  71. $this->response_headers[$this->response_headers_count] = '';
  72. }
  73. $this->response_headers[$this->response_headers_count] .= $buffer;
  74. }
  75. return $length;
  76. }
  77. /**
  78. * cURL callback to passthrough the HTTP body to the client.
  79. *
  80. * If the function return -1, curl stop to read the HTTP response
  81. *
  82. * @param resource $ch cURL handler
  83. * @param string $buffer Chunk of data
  84. *
  85. * @return int Length of the buffer
  86. */
  87. public function passthroughBody($ch, $buffer)
  88. {
  89. // do it only at the beginning of a transmission
  90. if ($this->body_length === 0) {
  91. list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1]));
  92. if ($this->isRedirection($status)) {
  93. return $this->handleRedirection($headers['Location']);
  94. }
  95. if (isset($headers['Content-Type'])) {
  96. header('Content-Type:' .$headers['Content-Type']);
  97. }
  98. }
  99. $length = strlen($buffer);
  100. $this->body_length += $length;
  101. echo $buffer;
  102. return $length;
  103. }
  104. /**
  105. * Prepare HTTP headers.
  106. *
  107. * @return string[]
  108. */
  109. private function prepareHeaders()
  110. {
  111. $headers = array(
  112. 'Connection: close',
  113. );
  114. if ($this->etag) {
  115. $headers[] = 'If-None-Match: '.$this->etag;
  116. $headers[] = 'A-IM: feed';
  117. }
  118. if ($this->last_modified) {
  119. $headers[] = 'If-Modified-Since: '.$this->last_modified;
  120. }
  121. $headers = array_merge($headers, $this->request_headers);
  122. return $headers;
  123. }
  124. /**
  125. * Prepare curl proxy context.
  126. *
  127. * @param resource $ch
  128. *
  129. * @return resource $ch
  130. */
  131. private function prepareProxyContext($ch)
  132. {
  133. if ($this->proxy_hostname) {
  134. Logger::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port);
  135. curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxy_port);
  136. curl_setopt($ch, CURLOPT_PROXYTYPE, 'HTTP');
  137. curl_setopt($ch, CURLOPT_PROXY, $this->proxy_hostname);
  138. if ($this->proxy_username) {
  139. Logger::setMessage(get_called_class().' Proxy credentials: Yes');
  140. curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->proxy_username.':'.$this->proxy_password);
  141. } else {
  142. Logger::setMessage(get_called_class().' Proxy credentials: No');
  143. }
  144. }
  145. return $ch;
  146. }
  147. /**
  148. * Prepare curl auth context.
  149. *
  150. * @param resource $ch
  151. *
  152. * @return resource $ch
  153. */
  154. private function prepareAuthContext($ch)
  155. {
  156. if ($this->username && $this->password) {
  157. curl_setopt($ch, CURLOPT_USERPWD, $this->username.':'.$this->password);
  158. }
  159. return $ch;
  160. }
  161. /**
  162. * Set write/header functions.
  163. *
  164. * @param resource $ch
  165. *
  166. * @return resource $ch
  167. */
  168. private function prepareDownloadMode($ch)
  169. {
  170. $this->body = '';
  171. $this->response_headers = array();
  172. $this->response_headers_count = 0;
  173. $write_function = 'readBody';
  174. $header_function = 'readHeaders';
  175. if ($this->isPassthroughEnabled()) {
  176. $write_function = 'passthroughBody';
  177. }
  178. curl_setopt($ch, CURLOPT_WRITEFUNCTION, array($this, $write_function));
  179. curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, $header_function));
  180. return $ch;
  181. }
  182. /**
  183. * Set additional CURL options.
  184. *
  185. * @param resource $ch
  186. *
  187. * @return resource $ch
  188. */
  189. private function prepareAdditionalCurlOptions($ch){
  190. foreach( $this->additional_curl_options as $c_op => $c_val ){
  191. curl_setopt($ch, $c_op, $c_val);
  192. }
  193. return $ch;
  194. }
  195. /**
  196. * Prepare curl context.
  197. *
  198. * @return resource
  199. */
  200. private function prepareContext()
  201. {
  202. $ch = curl_init();
  203. curl_setopt($ch, CURLOPT_URL, $this->url);
  204. curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
  205. curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
  206. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
  207. curl_setopt($ch, CURLOPT_USERAGENT, $this->user_agent);
  208. curl_setopt($ch, CURLOPT_HTTPHEADER, $this->prepareHeaders());
  209. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
  210. curl_setopt($ch, CURLOPT_ENCODING, '');
  211. curl_setopt($ch, CURLOPT_COOKIEJAR, 'php://memory');
  212. curl_setopt($ch, CURLOPT_COOKIEFILE, 'php://memory');
  213. // Disable SSLv3 by enforcing TLSv1.x for curl >= 7.34.0 and < 7.39.0.
  214. // Versions prior to 7.34 and at least when compiled against openssl
  215. // interpret this parameter as "limit to TLSv1.0" which fails for sites
  216. // which enforce TLS 1.1+.
  217. // Starting with curl 7.39.0 SSLv3 is disabled by default.
  218. $version = curl_version();
  219. if ($version['version_number'] >= 467456 && $version['version_number'] < 468736) {
  220. curl_setopt($ch, CURLOPT_SSLVERSION, 1);
  221. }
  222. $ch = $this->prepareDownloadMode($ch);
  223. $ch = $this->prepareProxyContext($ch);
  224. $ch = $this->prepareAuthContext($ch);
  225. $ch = $this->prepareAdditionalCurlOptions($ch);
  226. return $ch;
  227. }
  228. /**
  229. * Execute curl context.
  230. */
  231. private function executeContext()
  232. {
  233. $ch = $this->prepareContext();
  234. curl_exec($ch);
  235. Logger::setMessage(get_called_class().' cURL total time: '.curl_getinfo($ch, CURLINFO_TOTAL_TIME));
  236. Logger::setMessage(get_called_class().' cURL dns lookup time: '.curl_getinfo($ch, CURLINFO_NAMELOOKUP_TIME));
  237. Logger::setMessage(get_called_class().' cURL connect time: '.curl_getinfo($ch, CURLINFO_CONNECT_TIME));
  238. Logger::setMessage(get_called_class().' cURL speed download: '.curl_getinfo($ch, CURLINFO_SPEED_DOWNLOAD));
  239. Logger::setMessage(get_called_class().' cURL effective url: '.curl_getinfo($ch, CURLINFO_EFFECTIVE_URL));
  240. $curl_errno = curl_errno($ch);
  241. if ($curl_errno) {
  242. Logger::setMessage(get_called_class().' cURL error: '.curl_error($ch));
  243. curl_close($ch);
  244. $this->handleError($curl_errno);
  245. }
  246. // Update the url if there where redirects
  247. $this->url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
  248. curl_close($ch);
  249. }
  250. /**
  251. * Do the HTTP request.
  252. *
  253. * @return array HTTP response ['body' => ..., 'status' => ..., 'headers' => ...]
  254. */
  255. public function doRequest()
  256. {
  257. $this->executeContext();
  258. list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1]));
  259. if ($this->isRedirection($status)) {
  260. if (empty($headers['Location'])) {
  261. $status = 200;
  262. } else {
  263. return $this->handleRedirection($headers['Location']);
  264. }
  265. }
  266. return array(
  267. 'status' => $status,
  268. 'body' => $this->body,
  269. 'headers' => $headers,
  270. );
  271. }
  272. /**
  273. * Handle HTTP redirects
  274. *
  275. * @param string $location Redirected URL
  276. * @return array
  277. * @throws MaxRedirectException
  278. */
  279. private function handleRedirection($location)
  280. {
  281. $result = array();
  282. $this->url = Url::resolve($location, $this->url);
  283. $this->body = '';
  284. $this->body_length = 0;
  285. $this->response_headers = array();
  286. $this->response_headers_count = 0;
  287. while (true) {
  288. $this->nbRedirects++;
  289. if ($this->nbRedirects >= $this->max_redirects) {
  290. throw new MaxRedirectException('Maximum number of redirections reached');
  291. }
  292. $result = $this->doRequest();
  293. if ($this->isRedirection($result['status'])) {
  294. $this->url = Url::resolve($result['headers']['Location'], $this->url);
  295. $this->body = '';
  296. $this->body_length = 0;
  297. $this->response_headers = array();
  298. $this->response_headers_count = 0;
  299. } else {
  300. break;
  301. }
  302. }
  303. return $result;
  304. }
  305. /**
  306. * Handle cURL errors (throw individual exceptions).
  307. *
  308. * We don't use constants because they are not necessary always available
  309. * (depends of the version of libcurl linked to php)
  310. *
  311. * @see http://curl.haxx.se/libcurl/c/libcurl-errors.html
  312. *
  313. * @param int $errno cURL error code
  314. * @throws InvalidCertificateException
  315. * @throws InvalidUrlException
  316. * @throws MaxRedirectException
  317. * @throws MaxSizeException
  318. * @throws TimeoutException
  319. */
  320. private function handleError($errno)
  321. {
  322. switch ($errno) {
  323. case 78: // CURLE_REMOTE_FILE_NOT_FOUND
  324. throw new InvalidUrlException('Resource not found', $errno);
  325. case 6: // CURLE_COULDNT_RESOLVE_HOST
  326. throw new InvalidUrlException('Unable to resolve hostname', $errno);
  327. case 7: // CURLE_COULDNT_CONNECT
  328. throw new InvalidUrlException('Unable to connect to the remote host', $errno);
  329. case 23: // CURLE_WRITE_ERROR
  330. throw new MaxSizeException('Maximum response size exceeded', $errno);
  331. case 28: // CURLE_OPERATION_TIMEDOUT
  332. throw new TimeoutException('Operation timeout', $errno);
  333. case 35: // CURLE_SSL_CONNECT_ERROR
  334. case 51: // CURLE_PEER_FAILED_VERIFICATION
  335. case 58: // CURLE_SSL_CERTPROBLEM
  336. case 60: // CURLE_SSL_CACERT
  337. case 59: // CURLE_SSL_CIPHER
  338. case 64: // CURLE_USE_SSL_FAILED
  339. case 66: // CURLE_SSL_ENGINE_INITFAILED
  340. case 77: // CURLE_SSL_CACERT_BADFILE
  341. case 83: // CURLE_SSL_ISSUER_ERROR
  342. $msg = 'Invalid SSL certificate caused by CURL error number ' . $errno;
  343. throw new InvalidCertificateException($msg, $errno);
  344. case 47: // CURLE_TOO_MANY_REDIRECTS
  345. throw new MaxRedirectException('Maximum number of redirections reached', $errno);
  346. case 63: // CURLE_FILESIZE_EXCEEDED
  347. throw new MaxSizeException('Maximum response size exceeded', $errno);
  348. default:
  349. throw new InvalidUrlException('Unable to fetch the URL', $errno);
  350. }
  351. }
  352. }