PageRenderTime 51ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/core/Http.php

https://github.com/quarkness/piwik
PHP | 491 lines | 347 code | 51 blank | 93 comment | 84 complexity | e2889fade4f2e785638c461b66888acc MD5 | raw file
  1. <?php
  2. /**
  3. * Piwik - Open source web analytics
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. * @version $Id$
  8. *
  9. * @category Piwik
  10. * @package Piwik
  11. */
  12. /**
  13. * Server-side http client to retrieve content from remote servers, and optionally save to a local file.
  14. * Used to check for the latest Piwik version and download updates.
  15. *
  16. * @package Piwik
  17. */
  18. class Piwik_Http
  19. {
  20. /**
  21. * Get "best" available transport method for sendHttpRequest() calls.
  22. *
  23. * @return string
  24. */
  25. static public function getTransportMethod()
  26. {
  27. $method = 'curl';
  28. if(!extension_loaded('curl'))
  29. {
  30. $method = 'fopen';
  31. if(@ini_get('allow_url_fopen') != '1')
  32. {
  33. $method = 'socket';
  34. if(!function_exists('fsockopen'))
  35. {
  36. return null;
  37. }
  38. }
  39. }
  40. return $method;
  41. }
  42. /**
  43. * Sends http request ensuring the request will fail before $timeout seconds
  44. *
  45. * If no $destinationPath is specified, the trimmed response (without header) is returned as a string.
  46. * If a $destinationPath is specified, the response (without header) is saved to a file.
  47. *
  48. * @param string $aUrl
  49. * @param int $timeout
  50. * @param string $userAgent
  51. * @param string $destinationPath
  52. * @param int $followDepth
  53. * @return bool true (or string) on success; false on HTTP response error code (1xx or 4xx)
  54. * @throws Exception for all other errors
  55. */
  56. static public function sendHttpRequest($aUrl, $timeout, $userAgent = null, $destinationPath = null, $followDepth = 0, $acceptLanguage = false)
  57. {
  58. // create output file
  59. $file = null;
  60. if($destinationPath)
  61. {
  62. // Ensure destination directory exists
  63. Piwik_Common::mkdir(dirname($destinationPath));
  64. if (($file = @fopen($destinationPath, 'wb')) === false || !is_resource($file))
  65. {
  66. throw new Exception('Error while creating the file: ' . $destinationPath);
  67. }
  68. }
  69. return self::sendHttpRequestBy(self::getTransportMethod(), $aUrl, $timeout, $userAgent, $destinationPath, $file, $followDepth, $acceptLanguage);
  70. }
  71. /**
  72. * Sends http request using the specified transport method
  73. *
  74. * @param string $method
  75. * @param string $aUrl
  76. * @param int $timeout
  77. * @param string $userAgent
  78. * @param string $destinationPath
  79. * @param resource $file
  80. * @param int $followDepth
  81. * @return bool true (or string) on success; false on HTTP response error code (1xx or 4xx)
  82. * @throws Exception for all other errors
  83. */
  84. static public function sendHttpRequestBy($method = 'socket', $aUrl, $timeout, $userAgent = null, $destinationPath = null, $file = null, $followDepth = 0, $acceptLanguage = false)
  85. {
  86. if ($followDepth > 5)
  87. {
  88. throw new Exception('Too many redirects ('.$followDepth.')');
  89. }
  90. $contentLength = 0;
  91. $fileLength = 0;
  92. // Piwik services behave like a proxy, so we should act like one.
  93. $xff = 'X-Forwarded-For: '
  94. . (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] . ',' : '')
  95. . Piwik_IP::getIpFromHeader();
  96. $via = 'Via: '
  97. . (isset($_SERVER['HTTP_VIA']) && !empty($_SERVER['HTTP_VIA']) ? $_SERVER['HTTP_VIA'] . ', ' : '')
  98. . Piwik_Version::VERSION . ' Piwik'
  99. . ($userAgent ? " ($userAgent)" : '');
  100. $acceptLanguage = $acceptLanguage ? 'Accept-Language:'.$acceptLanguage : '';
  101. $userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'Piwik/'.Piwik_Version::VERSION;
  102. // proxy configuration
  103. if(!empty($GLOBALS['PIWIK_TRACKER_MODE']))
  104. {
  105. $proxyHost = Piwik_Tracker_Config::getInstance()->proxy['host'];
  106. $proxyPort = Piwik_Tracker_Config::getInstance()->proxy['port'];
  107. $proxyUser = Piwik_Tracker_Config::getInstance()->proxy['username'];
  108. $proxyPassword = Piwik_Tracker_Config::getInstance()->proxy['password'];
  109. }
  110. else
  111. {
  112. $config = Zend_Registry::get('config');
  113. if($config !== false)
  114. {
  115. $proxyHost = $config->proxy->host;
  116. $proxyPort = $config->proxy->port;
  117. $proxyUser = $config->proxy->username;
  118. $proxyPassword = $config->proxy->password;
  119. }
  120. }
  121. if($method == 'socket')
  122. {
  123. // initialization
  124. $url = @parse_url($aUrl);
  125. if($url === false || !isset($url['scheme']))
  126. {
  127. throw new Exception('Malformed URL: '.$aUrl);
  128. }
  129. if($url['scheme'] != 'http')
  130. {
  131. throw new Exception('Invalid protocol/scheme: '.$url['scheme']);
  132. }
  133. $host = $url['host'];
  134. $port = isset($url['port)']) ? $url['port'] : 80;
  135. $path = isset($url['path']) ? $url['path'] : '/';
  136. if(isset($url['query']))
  137. {
  138. $path .= '?'.$url['query'];
  139. }
  140. $errno = null;
  141. $errstr = null;
  142. $proxyAuth = null;
  143. if(!empty($proxyHost) && !empty($proxyPort))
  144. {
  145. $connectHost = $proxyHost;
  146. $connectPort = $proxyPort;
  147. if(!empty($proxyUser) && !empty($proxyPassword))
  148. {
  149. $proxyAuth = 'Proxy-Authorization: Basic '.base64_encode("$proxyUser:$proxyPassword") ."\r\n";
  150. }
  151. $requestHeader = "GET $aUrl HTTP/1.1\r\n";
  152. }
  153. else
  154. {
  155. $connectHost = $host;
  156. $connectPort = $port;
  157. $requestHeader = "GET $path HTTP/1.0\r\n";
  158. }
  159. // connection attempt
  160. if (($fsock = @fsockopen($connectHost, $connectPort, $errno, $errstr, $timeout)) === false || !is_resource($fsock))
  161. {
  162. if(is_resource($file)) { @fclose($file); }
  163. throw new Exception("Error while connecting to: $host. Please try again later. $errstr");
  164. }
  165. // send HTTP request header
  166. $requestHeader .=
  167. "Host: $host".($port != 80 ? ':'.$port : '')."\r\n"
  168. .($proxyAuth ? $proxyAuth : '')
  169. .'User-Agent: '.$userAgent."\r\n"
  170. . ($acceptLanguage ? $acceptLanguage ."\r\n" : '')
  171. .$xff."\r\n"
  172. .$via."\r\n"
  173. ."Connection: close\r\n"
  174. ."\r\n";
  175. fwrite($fsock, $requestHeader);
  176. $streamMetaData = array('timed_out' => false);
  177. @stream_set_blocking($fsock, true);
  178. if (function_exists('stream_set_timeout'))
  179. {
  180. @stream_set_timeout($fsock, $timeout);
  181. }
  182. elseif (function_exists('socket_set_timeout'))
  183. {
  184. @socket_set_timeout($fsock, $timeout);
  185. }
  186. // process header
  187. $status = null;
  188. $expectRedirect = false;
  189. while(!feof($fsock))
  190. {
  191. $line = fgets($fsock, 4096);
  192. $streamMetaData = @stream_get_meta_data($fsock);
  193. if($streamMetaData['timed_out'])
  194. {
  195. if(is_resource($file)) { @fclose($file); }
  196. @fclose($fsock);
  197. throw new Exception('Timed out waiting for server response');
  198. }
  199. // a blank line marks the end of the server response header
  200. if(rtrim($line, "\r\n") == '')
  201. {
  202. break;
  203. }
  204. // parse first line of server response header
  205. if(!$status)
  206. {
  207. // expect first line to be HTTP response status line, e.g., HTTP/1.1 200 OK
  208. if(!preg_match('~^HTTP/(\d\.\d)\s+(\d+)(\s*.*)?~', $line, $m))
  209. {
  210. if(is_resource($file)) { @fclose($file); }
  211. @fclose($fsock);
  212. throw new Exception('Expected server response code. Got '.rtrim($line, "\r\n"));
  213. }
  214. $status = (integer) $m[2];
  215. // Informational 1xx or Client Error 4xx
  216. if ($status < 200 || $status >= 400)
  217. {
  218. if(is_resource($file)) { @fclose($file); }
  219. @fclose($fsock);
  220. return false;
  221. }
  222. continue;
  223. }
  224. // handle redirect
  225. if(preg_match('/^Location:\s*(.+)/', rtrim($line, "\r\n"), $m))
  226. {
  227. if(is_resource($file)) { @fclose($file); }
  228. @fclose($fsock);
  229. // Successful 2xx vs Redirect 3xx
  230. if($status < 300)
  231. {
  232. throw new Exception('Unexpected redirect to Location: '.rtrim($line).' for status code '.$status);
  233. }
  234. return self::sendHttpRequestBy($method, trim($m[1]), $timeout, $userAgent, $destinationPath, $file, $followDepth+1, $acceptLanguage);
  235. }
  236. // save expected content length for later verification
  237. if(preg_match('/^Content-Length:\s*(\d+)/', $line, $m))
  238. {
  239. $contentLength = (integer) $m[1];
  240. }
  241. }
  242. if(feof($fsock))
  243. {
  244. throw new Exception('Unexpected end of transmission');
  245. }
  246. // process content/body
  247. $response = '';
  248. while(!feof($fsock))
  249. {
  250. $line = fread($fsock, 8192);
  251. $streamMetaData = @stream_get_meta_data($fsock);
  252. if($streamMetaData['timed_out'])
  253. {
  254. if(is_resource($file)) { @fclose($file); }
  255. @fclose($fsock);
  256. throw new Exception('Timed out waiting for server response');
  257. }
  258. $fileLength += Piwik_Common::strlen($line);
  259. if(is_resource($file))
  260. {
  261. // save to file
  262. fwrite($file, $line);
  263. }
  264. else
  265. {
  266. // concatenate to response string
  267. $response .= $line;
  268. }
  269. }
  270. // determine success or failure
  271. @fclose(@$fsock);
  272. }
  273. else if($method == 'fopen')
  274. {
  275. $response = false;
  276. // we make sure the request takes less than a few seconds to fail
  277. // we create a stream_context (works in php >= 5.2.1)
  278. // we also set the socket_timeout (for php < 5.2.1)
  279. $default_socket_timeout = @ini_get('default_socket_timeout');
  280. @ini_set('default_socket_timeout', $timeout);
  281. $ctx = null;
  282. if(function_exists('stream_context_create')) {
  283. $stream_options = array(
  284. 'http' => array(
  285. 'header' => 'User-Agent: '.$userAgent."\r\n"
  286. .($acceptLanguage ? $acceptLanguage."\r\n" : '')
  287. .$xff."\r\n"
  288. .$via."\r\n",
  289. 'max_redirects' => 5, // PHP 5.1.0
  290. 'timeout' => $timeout, // PHP 5.2.1
  291. )
  292. );
  293. if(!empty($proxyHost) && !empty($proxyPort))
  294. {
  295. $stream_options['http']['proxy'] = 'tcp://'.$proxyHost.':'.$proxyPort;
  296. $stream_options['http']['request_fulluri'] = true; // required by squid proxy
  297. if(!empty($proxyUser) && !empty($proxyPassword))
  298. {
  299. $stream_options['http']['header'] .= 'Proxy-Authorization: Basic '.base64_encode("$proxyUser:$proxyPassword")."\r\n";
  300. }
  301. }
  302. $ctx = stream_context_create($stream_options);
  303. }
  304. // save to file
  305. if(is_resource($file))
  306. {
  307. $handle = fopen($aUrl, 'rb', false, $ctx);
  308. while(!feof($handle))
  309. {
  310. $response = fread($handle, 8192);
  311. $fileLength += Piwik_Common::strlen($response);
  312. fwrite($file, $response);
  313. }
  314. fclose($handle);
  315. }
  316. else
  317. {
  318. $response = @file_get_contents($aUrl, 0, $ctx);
  319. $fileLength = Piwik_Common::strlen($response);
  320. }
  321. // restore the socket_timeout value
  322. if(!empty($default_socket_timeout))
  323. {
  324. @ini_set('default_socket_timeout', $default_socket_timeout);
  325. }
  326. }
  327. else if($method == 'curl')
  328. {
  329. $ch = @curl_init();
  330. if(!empty($proxyHost) && !empty($proxyPort))
  331. {
  332. @curl_setopt($ch, CURLOPT_PROXY, $proxyHost.':'.$proxyPort);
  333. if(!empty($proxyUser) && !empty($proxyPassword))
  334. {
  335. // PROXYAUTH defaults to BASIC
  336. @curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyUser.':'.$proxyPassword);
  337. }
  338. }
  339. $curl_options = array(
  340. // internal to ext/curl
  341. CURLOPT_BINARYTRANSFER => is_resource($file),
  342. // curl options (sorted oldest to newest)
  343. CURLOPT_URL => $aUrl,
  344. CURLOPT_USERAGENT => $userAgent,
  345. CURLOPT_HTTPHEADER => array(
  346. $xff,
  347. $via,
  348. $acceptLanguage
  349. ),
  350. CURLOPT_HEADER => false,
  351. CURLOPT_CONNECTTIMEOUT => $timeout,
  352. );
  353. @curl_setopt_array($ch, $curl_options);
  354. /*
  355. * use local list of Certificate Authorities, if available
  356. */
  357. if(file_exists(PIWIK_INCLUDE_PATH . '/core/DataFiles/cacert.pem'))
  358. {
  359. @curl_setopt($ch, CURLOPT_CAINFO, PIWIK_INCLUDE_PATH . '/core/DataFiles/cacert.pem');
  360. }
  361. /*
  362. * as of php 5.2.0, CURLOPT_FOLLOWLOCATION can't be set if
  363. * in safe_mode or open_basedir is set
  364. */
  365. if((string)ini_get('safe_mode') == '' && ini_get('open_basedir') == '')
  366. {
  367. $curl_options = array(
  368. // curl options (sorted oldest to newest)
  369. CURLOPT_FOLLOWLOCATION => true,
  370. CURLOPT_MAXREDIRS => 5,
  371. );
  372. @curl_setopt_array($ch, $curl_options);
  373. }
  374. if(is_resource($file))
  375. {
  376. // write output directly to file
  377. @curl_setopt($ch, CURLOPT_FILE, $file);
  378. }
  379. else
  380. {
  381. // internal to ext/curl
  382. @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  383. }
  384. ob_start();
  385. $response = @curl_exec($ch);
  386. ob_end_clean();
  387. if($response === true)
  388. {
  389. $response = '';
  390. }
  391. else if($response === false)
  392. {
  393. $errstr = curl_error($ch);
  394. if($errstr != '')
  395. {
  396. throw new Exception('curl_exec: '.$errstr);
  397. }
  398. $response = '';
  399. }
  400. $contentLength = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
  401. $fileLength = is_resource($file) ? curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) : Piwik_Common::strlen($response);
  402. @curl_close($ch);
  403. unset($ch);
  404. }
  405. else
  406. {
  407. throw new Exception('Invalid request method: '.$method);
  408. }
  409. if(is_resource($file))
  410. {
  411. fflush($file);
  412. @fclose($file);
  413. $fileSize = filesize($destinationPath);
  414. if((($contentLength > 0) && ($fileLength != $contentLength)) || ($fileSize != $fileLength))
  415. {
  416. throw new Exception('File size error: '.$destinationPath.'; expected '.$contentLength.' bytes; received '.$fileLength.' bytes; saved '.$fileSize.' bytes to file');
  417. }
  418. return true;
  419. }
  420. if(($contentLength > 0) && ($fileLength != $contentLength))
  421. {
  422. throw new Exception('Content length error: expected '.$contentLength.' bytes; received '.$fileLength.' bytes');
  423. }
  424. return trim($response);
  425. }
  426. /**
  427. * Fetch the file at $url in the destination $destinationPath
  428. *
  429. * @param string $url
  430. * @param string $destinationPath
  431. * @param int $tries
  432. * @return true on success, throws Exception on failure
  433. */
  434. static public function fetchRemoteFile($url, $destinationPath = null, $tries = 0)
  435. {
  436. @ignore_user_abort(true);
  437. Piwik::setMaxExecutionTime(0);
  438. return self::sendHttpRequest($url, 10, 'Update', $destinationPath, $tries);
  439. }
  440. }