HttpDownloader.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Util;
  12. use Composer\Config;
  13. use Composer\IO\IOInterface;
  14. use Composer\Downloader\TransportException;
  15. use Composer\CaBundle\CaBundle;
  16. use Composer\Util\Http\Response;
  17. use Psr\Log\LoggerInterface;
  18. use React\Promise\Promise;
  19. /**
  20. * @author Jordi Boggiano <j.boggiano@seld.be>
  21. */
  22. class HttpDownloader
  23. {
  24. const STATUS_QUEUED = 1;
  25. const STATUS_STARTED = 2;
  26. const STATUS_COMPLETED = 3;
  27. const STATUS_FAILED = 4;
  28. private $io;
  29. private $config;
  30. private $jobs = array();
  31. private $options = array();
  32. private $runningJobs = 0;
  33. private $maxJobs = 10;
  34. private $lastProgress;
  35. private $disableTls = false;
  36. private $curl;
  37. private $rfs;
  38. private $idGen = 0;
  39. private $disabled;
  40. /**
  41. * @param IOInterface $io The IO instance
  42. * @param Config $config The config
  43. * @param array $options The options
  44. * @param bool $disableTls
  45. */
  46. public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false)
  47. {
  48. $this->io = $io;
  49. $this->disabled = (bool) getenv('COMPOSER_DISABLE_NETWORK');
  50. // Setup TLS options
  51. // The cafile option can be set via config.json
  52. if ($disableTls === false) {
  53. $logger = $io instanceof LoggerInterface ? $io : null;
  54. $this->options = StreamContextFactory::getTlsDefaults($options, $logger);
  55. } else {
  56. $this->disableTls = true;
  57. }
  58. // handle the other externally set options normally.
  59. $this->options = array_replace_recursive($this->options, $options);
  60. $this->config = $config;
  61. // TODO enable curl only on 5.6+ if older versions cause any problem
  62. if (extension_loaded('curl')) {
  63. $this->curl = new Http\CurlDownloader($io, $config, $options, $disableTls);
  64. }
  65. $this->rfs = new RemoteFilesystem($io, $config, $options, $disableTls);
  66. }
  67. public function get($url, $options = array())
  68. {
  69. list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false), true);
  70. $this->wait($job['id']);
  71. return $this->getResponse($job['id']);
  72. }
  73. public function add($url, $options = array())
  74. {
  75. list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false));
  76. return $promise;
  77. }
  78. public function copy($url, $to, $options = array())
  79. {
  80. list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => $to), true);
  81. $this->wait($job['id']);
  82. return $this->getResponse($job['id']);
  83. }
  84. public function addCopy($url, $to, $options = array())
  85. {
  86. list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => $to));
  87. return $promise;
  88. }
  89. /**
  90. * Retrieve the options set in the constructor
  91. *
  92. * @return array Options
  93. */
  94. public function getOptions()
  95. {
  96. return $this->options;
  97. }
  98. /**
  99. * Merges new options
  100. *
  101. * @return array $options
  102. */
  103. public function setOptions(array $options)
  104. {
  105. $this->options = array_replace_recursive($this->options, $options);
  106. }
  107. private function addJob($request, $sync = false)
  108. {
  109. $job = array(
  110. 'id' => $this->idGen++,
  111. 'status' => self::STATUS_QUEUED,
  112. 'request' => $request,
  113. 'sync' => $sync,
  114. 'origin' => Url::getOrigin($this->config, $request['url']),
  115. );
  116. // capture username/password from URL if there is one
  117. if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $request['url'], $match)) {
  118. $this->io->setAuthentication($job['origin'], rawurldecode($match[1]), rawurldecode($match[2]));
  119. }
  120. $rfs = $this->rfs;
  121. if ($this->curl && preg_match('{^https?://}i', $job['request']['url'])) {
  122. $resolver = function ($resolve, $reject) use (&$job) {
  123. $job['status'] = HttpDownloader::STATUS_QUEUED;
  124. $job['resolve'] = $resolve;
  125. $job['reject'] = $reject;
  126. };
  127. } else {
  128. $resolver = function ($resolve, $reject) use (&$job, $rfs) {
  129. // start job
  130. $url = $job['request']['url'];
  131. $options = $job['request']['options'];
  132. $job['status'] = HttpDownloader::STATUS_STARTED;
  133. if ($job['request']['copyTo']) {
  134. $result = $rfs->copy($job['origin'], $url, $job['request']['copyTo'], false /* TODO progress */, $options);
  135. $resolve($result);
  136. } else {
  137. $body = $rfs->getContents($job['origin'], $url, false /* TODO progress */, $options);
  138. $headers = $rfs->getLastHeaders();
  139. $response = new Http\Response($job['request'], $rfs->findStatusCode($headers), $headers, $body);
  140. $resolve($response);
  141. }
  142. };
  143. }
  144. $downloader = $this;
  145. $io = $this->io;
  146. $canceler = function () {};
  147. $promise = new Promise($resolver, $canceler);
  148. $promise->then(function ($response) use (&$job, $downloader) {
  149. $job['status'] = HttpDownloader::STATUS_COMPLETED;
  150. $job['response'] = $response;
  151. // TODO 3.0 this should be done directly on $this when PHP 5.3 is dropped
  152. $downloader->markJobDone();
  153. $downloader->scheduleNextJob();
  154. return $response;
  155. }, function ($e) use ($io, &$job, $downloader) {
  156. $job['status'] = HttpDownloader::STATUS_FAILED;
  157. $job['exception'] = $e;
  158. $downloader->markJobDone();
  159. throw $e;
  160. });
  161. $this->jobs[$job['id']] =& $job;
  162. if ($this->runningJobs < $this->maxJobs) {
  163. $this->startJob($job['id']);
  164. }
  165. return array($job, $promise);
  166. }
  167. private function startJob($id)
  168. {
  169. $job =& $this->jobs[$id];
  170. if ($job['status'] !== self::STATUS_QUEUED) {
  171. return;
  172. }
  173. // start job
  174. $job['status'] = self::STATUS_STARTED;
  175. $this->runningJobs++;
  176. $resolve = $job['resolve'];
  177. $reject = $job['reject'];
  178. $url = $job['request']['url'];
  179. $options = $job['request']['options'];
  180. $origin = $job['origin'];
  181. if ($this->disabled) {
  182. if (isset($job['request']['options']['http']['header']) && false !== stripos(implode('', $job['request']['options']['http']['header']), 'if-modified-since')) {
  183. $resolve(new Response(array('url' => $url), 304, array(), ''));
  184. } else {
  185. $e = new TransportException('Network disabled', 499);
  186. $e->setStatusCode(499);
  187. $reject($e);
  188. }
  189. return;
  190. }
  191. if ($job['request']['copyTo']) {
  192. $this->curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']);
  193. } else {
  194. $this->curl->download($resolve, $reject, $origin, $url, $options);
  195. }
  196. }
  197. /**
  198. * @private
  199. */
  200. public function markJobDone()
  201. {
  202. $this->runningJobs--;
  203. }
  204. /**
  205. * @private
  206. */
  207. public function scheduleNextJob()
  208. {
  209. foreach ($this->jobs as $job) {
  210. if ($job['status'] === self::STATUS_QUEUED) {
  211. $this->startJob($job['id']);
  212. if ($this->runningJobs >= $this->maxJobs) {
  213. return;
  214. }
  215. }
  216. }
  217. }
  218. public function wait($index = null, $progress = false)
  219. {
  220. while (true) {
  221. if ($this->curl) {
  222. $this->curl->tick();
  223. }
  224. if (null !== $index) {
  225. if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED) {
  226. return;
  227. }
  228. } else {
  229. $done = true;
  230. foreach ($this->jobs as $job) {
  231. if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED), true)) {
  232. $done = false;
  233. break;
  234. } elseif (!$job['sync']) {
  235. unset($this->jobs[$job['id']]);
  236. }
  237. }
  238. if ($done) {
  239. return;
  240. }
  241. }
  242. usleep(1000);
  243. }
  244. }
  245. private function getResponse($index)
  246. {
  247. if (!isset($this->jobs[$index])) {
  248. throw new \LogicException('Invalid request id');
  249. }
  250. if ($this->jobs[$index]['status'] === self::STATUS_FAILED) {
  251. throw $this->jobs[$index]['exception'];
  252. }
  253. if (!isset($this->jobs[$index]['response'])) {
  254. throw new \LogicException('Response not available yet, call wait() first');
  255. }
  256. $resp = $this->jobs[$index]['response'];
  257. unset($this->jobs[$index]);
  258. return $resp;
  259. }
  260. }