SdkCurlFactory.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. <?php
  2. /**
  3. * Copyright (c) 2011-2018 Michael Dowling, https://github.com/mtdowling <mtdowling@gmail.com>
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. * The above copyright notice and this permission notice shall be included in
  11. * all copies or substantial portions of the Software.
  12. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  13. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  14. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  15. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  16. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  17. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  18. * THE SOFTWARE.
  19. */
  20. namespace Obs\Internal\Common;
  21. use GuzzleHttp\Psr7;
  22. use GuzzleHttp\Psr7\LazyOpenStream;
  23. use Psr\Http\Message\RequestInterface;
  24. use GuzzleHttp\Handler\CurlFactoryInterface;
  25. use GuzzleHttp\Handler\EasyHandle;
  26. class SdkCurlFactory implements CurlFactoryInterface
  27. {
  28. private $handles = [];
  29. private $maxHandles;
  30. public function __construct($maxHandles)
  31. {
  32. $this->maxHandles = $maxHandles;
  33. }
  34. public function create(RequestInterface $request, array $options): EasyHandle
  35. {
  36. if (isset($options['curl']['body_as_string'])) {
  37. $options['_body_as_string'] = $options['curl']['body_as_string'];
  38. unset($options['curl']['body_as_string']);
  39. }
  40. $easy = new EasyHandle;
  41. $easy->request = $request;
  42. $easy->options = $options;
  43. $conf = $this->getDefaultConf($easy);
  44. $this->applyMethod($easy, $conf);
  45. $this->applyHandlerOptions($easy, $conf);
  46. $this->applyHeaders($easy, $conf);
  47. unset($conf['_headers']);
  48. if (isset($options['curl'])) {
  49. $conf = array_replace($conf, $options['curl']);
  50. }
  51. $conf[CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
  52. if($this->handles){
  53. $easy->handle = array_pop($this->handles);
  54. }else{
  55. $easy->handle = curl_init();
  56. }
  57. curl_setopt_array($easy->handle, $conf);
  58. return $easy;
  59. }
  60. public function close()
  61. {
  62. if($this->handles){
  63. foreach ($this->handles as $handle){
  64. curl_close($handle);
  65. }
  66. unset($this->handles);
  67. $this->handles = [];
  68. }
  69. }
  70. public function release(EasyHandle $easy): void
  71. {
  72. $resource = $easy->handle;
  73. unset($easy->handle);
  74. if (count($this->handles) >= $this->maxHandles) {
  75. curl_close($resource);
  76. } else {
  77. curl_setopt($resource, CURLOPT_HEADERFUNCTION, null);
  78. curl_setopt($resource, CURLOPT_READFUNCTION, null);
  79. curl_setopt($resource, CURLOPT_WRITEFUNCTION, null);
  80. curl_setopt($resource, CURLOPT_PROGRESSFUNCTION, null);
  81. curl_reset($resource);
  82. $this->handles[] = $resource;
  83. }
  84. }
  85. private function getDefaultConf(EasyHandle $easy)
  86. {
  87. $conf = [
  88. '_headers' => $easy->request->getHeaders(),
  89. CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(),
  90. CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''),
  91. CURLOPT_RETURNTRANSFER => false,
  92. CURLOPT_HEADER => false,
  93. CURLOPT_CONNECTTIMEOUT => 150,
  94. ];
  95. if (defined('CURLOPT_PROTOCOLS')) {
  96. $conf[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
  97. }
  98. $version = $easy->request->getProtocolVersion();
  99. if ($version == 1.1) {
  100. $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
  101. } elseif ($version == 2.0) {
  102. $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
  103. } else {
  104. $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
  105. }
  106. return $conf;
  107. }
  108. private function applyMethod(EasyHandle $easy, array &$conf)
  109. {
  110. $body = $easy->request->getBody();
  111. $size = $body->getSize();
  112. if ($size === null || $size > 0) {
  113. $this->applyBody($easy->request, $easy->options, $conf);
  114. return;
  115. }
  116. $method = $easy->request->getMethod();
  117. if ($method === 'PUT' || $method === 'POST') {
  118. if (!$easy->request->hasHeader('Content-Length')) {
  119. $conf[CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
  120. }
  121. } elseif ($method === 'HEAD') {
  122. $conf[CURLOPT_NOBODY] = true;
  123. unset(
  124. $conf[CURLOPT_WRITEFUNCTION],
  125. $conf[CURLOPT_READFUNCTION],
  126. $conf[CURLOPT_FILE],
  127. $conf[CURLOPT_INFILE]
  128. );
  129. }
  130. }
  131. private function applyBody(RequestInterface $request, array $options, array &$conf)
  132. {
  133. $size = $request->hasHeader('Content-Length')
  134. ? (int) $request->getHeaderLine('Content-Length')
  135. : $request->getBody()->getSize();
  136. if($request->getBody()->getSize() === $size && $request -> getBody() ->tell() <= 0){
  137. if (($size !== null && $size < 1000000) ||
  138. !empty($options['_body_as_string'])
  139. ) {
  140. $conf[CURLOPT_POSTFIELDS] = (string) $request->getBody();
  141. $this->removeHeader('Content-Length', $conf);
  142. $this->removeHeader('Transfer-Encoding', $conf);
  143. } else {
  144. $conf[CURLOPT_UPLOAD] = true;
  145. if ($size !== null) {
  146. $conf[CURLOPT_INFILESIZE] = $size;
  147. $this->removeHeader('Content-Length', $conf);
  148. }
  149. $body = $request->getBody();
  150. if ($body->isSeekable()) {
  151. $body->rewind();
  152. }
  153. $conf[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
  154. return $body->read($length);
  155. };
  156. }
  157. }else{
  158. $body = $request->getBody();
  159. $conf[CURLOPT_UPLOAD] = true;
  160. $conf[CURLOPT_INFILESIZE] = $size;
  161. $readCount = 0;
  162. $conf[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body, $readCount, $size) {
  163. if($readCount >= $size){
  164. $body -> close();
  165. return '';
  166. }
  167. $readCountOnce = $length <= $size ? $length : $size;
  168. $readCount += $readCountOnce;
  169. return $body->read($readCountOnce);
  170. };
  171. }
  172. if (!$request->hasHeader('Expect')) {
  173. $conf[CURLOPT_HTTPHEADER][] = 'Expect:';
  174. }
  175. if (!$request->hasHeader('Content-Type')) {
  176. $conf[CURLOPT_HTTPHEADER][] = 'Content-Type:';
  177. }
  178. }
  179. private function applyHeaders(EasyHandle $easy, array &$conf)
  180. {
  181. foreach ($conf['_headers'] as $name => $values) {
  182. foreach ($values as $value) {
  183. $conf[CURLOPT_HTTPHEADER][] = "$name: $value";
  184. }
  185. }
  186. // Remove the Accept header if one was not set
  187. if (!$easy->request->hasHeader('Accept')) {
  188. $conf[CURLOPT_HTTPHEADER][] = 'Accept:';
  189. }
  190. }
  191. private function removeHeader($name, array &$options)
  192. {
  193. foreach (array_keys($options['_headers']) as $key) {
  194. if (!strcasecmp($key, $name)) {
  195. unset($options['_headers'][$key]);
  196. return;
  197. }
  198. }
  199. }
  200. private function applyHandlerOptions(EasyHandle $easy, array &$conf)
  201. {
  202. $options = $easy->options;
  203. if (isset($options['verify'])) {
  204. $conf[CURLOPT_SSL_VERIFYHOST] = 0;
  205. if ($options['verify'] === false) {
  206. unset($conf[CURLOPT_CAINFO]);
  207. $conf[CURLOPT_SSL_VERIFYPEER] = false;
  208. } else {
  209. $conf[CURLOPT_SSL_VERIFYPEER] = true;
  210. if (is_string($options['verify'])) {
  211. if (!file_exists($options['verify'])) {
  212. throw new \InvalidArgumentException(
  213. "SSL CA bundle not found: {$options['verify']}"
  214. );
  215. }
  216. if (is_dir($options['verify']) ||
  217. (is_link($options['verify']) && is_dir(readlink($options['verify'])))) {
  218. $conf[CURLOPT_CAPATH] = $options['verify'];
  219. } else {
  220. $conf[CURLOPT_CAINFO] = $options['verify'];
  221. }
  222. }
  223. }
  224. }
  225. if (!empty($options['decode_content'])) {
  226. $accept = $easy->request->getHeaderLine('Accept-Encoding');
  227. if ($accept) {
  228. $conf[CURLOPT_ENCODING] = $accept;
  229. } else {
  230. $conf[CURLOPT_ENCODING] = '';
  231. $conf[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
  232. }
  233. }
  234. if (isset($options['sink'])) {
  235. $sink = $options['sink'];
  236. if (!is_string($sink)) {
  237. try {
  238. $sink = Psr7\stream_for($sink);
  239. } catch (\Throwable $e) {
  240. $sink = Psr7\Utils::streamFor($sink);
  241. }
  242. } elseif (!is_dir(dirname($sink))) {
  243. throw new \RuntimeException(sprintf(
  244. 'Directory %s does not exist for sink value of %s',
  245. dirname($sink),
  246. $sink
  247. ));
  248. } else {
  249. $sink = new LazyOpenStream($sink, 'w+');
  250. }
  251. $easy->sink = $sink;
  252. $conf[CURLOPT_WRITEFUNCTION] = function ($ch, $write) use ($sink) {
  253. return $sink->write($write);
  254. };
  255. } else {
  256. $conf[CURLOPT_FILE] = fopen('php://temp', 'w+');
  257. try {
  258. $easy->sink = Psr7\stream_for($conf[CURLOPT_FILE]);
  259. } catch (\Throwable $e) {
  260. $easy->sink = Psr7\Utils::streamFor($conf[CURLOPT_FILE]);
  261. }
  262. }
  263. $timeoutRequiresNoSignal = false;
  264. if (isset($options['timeout'])) {
  265. $timeoutRequiresNoSignal |= $options['timeout'] < 1;
  266. $conf[CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
  267. }
  268. if (isset($options['force_ip_resolve'])) {
  269. if ('v4' === $options['force_ip_resolve']) {
  270. $conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
  271. } else if ('v6' === $options['force_ip_resolve']) {
  272. $conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V6;
  273. }
  274. }
  275. if (isset($options['connect_timeout'])) {
  276. $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
  277. $conf[CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
  278. }
  279. if ($timeoutRequiresNoSignal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
  280. $conf[CURLOPT_NOSIGNAL] = true;
  281. }
  282. if (isset($options['proxy'])) {
  283. if (!is_array($options['proxy'])) {
  284. $conf[CURLOPT_PROXY] = $options['proxy'];
  285. } else {
  286. $scheme = $easy->request->getUri()->getScheme();
  287. if (isset($options['proxy'][$scheme])) {
  288. $host = $easy->request->getUri()->getHost();
  289. if (!isset($options['proxy']['no']) ||
  290. !\GuzzleHttp\is_host_in_noproxy($host, $options['proxy']['no'])
  291. ) {
  292. $conf[CURLOPT_PROXY] = $options['proxy'][$scheme];
  293. }
  294. }
  295. }
  296. }
  297. if (isset($options['cert'])) {
  298. $cert = $options['cert'];
  299. if (is_array($cert)) {
  300. $conf[CURLOPT_SSLCERTPASSWD] = $cert[1];
  301. $cert = $cert[0];
  302. }
  303. if (!file_exists($cert)) {
  304. throw new \InvalidArgumentException(
  305. "SSL certificate not found: {$cert}"
  306. );
  307. }
  308. $conf[CURLOPT_SSLCERT] = $cert;
  309. }
  310. if (isset($options['ssl_key'])) {
  311. $sslKey = $options['ssl_key'];
  312. if (is_array($sslKey)) {
  313. $conf[CURLOPT_SSLKEYPASSWD] = $sslKey[1];
  314. $sslKey = $sslKey[0];
  315. }
  316. if (!file_exists($sslKey)) {
  317. throw new \InvalidArgumentException(
  318. "SSL private key not found: {$sslKey}"
  319. );
  320. }
  321. $conf[CURLOPT_SSLKEY] = $sslKey;
  322. }
  323. if (isset($options['progress'])) {
  324. $progress = $options['progress'];
  325. if (!is_callable($progress)) {
  326. throw new \InvalidArgumentException(
  327. 'progress client option must be callable'
  328. );
  329. }
  330. $conf[CURLOPT_NOPROGRESS] = false;
  331. $conf[CURLOPT_PROGRESSFUNCTION] = function () use ($progress) {
  332. $args = func_get_args();
  333. if (is_resource($args[0])) {
  334. array_shift($args);
  335. }
  336. call_user_func_array($progress, $args);
  337. };
  338. }
  339. if (!empty($options['debug'])) {
  340. $conf[CURLOPT_STDERR] = \GuzzleHttp\debug_resource($options['debug']);
  341. $conf[CURLOPT_VERBOSE] = true;
  342. }
  343. }
  344. private function createHeaderFn(EasyHandle $easy)
  345. {
  346. if (isset($easy->options['on_headers'])) {
  347. $onHeaders = $easy->options['on_headers'];
  348. if (!is_callable($onHeaders)) {
  349. throw new \InvalidArgumentException('on_headers must be callable');
  350. }
  351. } else {
  352. $onHeaders = null;
  353. }
  354. return function ($ch, $h) use (
  355. $onHeaders,
  356. $easy,
  357. &$startingResponse
  358. ) {
  359. $value = trim($h);
  360. if ($value === '') {
  361. $startingResponse = true;
  362. $easy->createResponse();
  363. if ($onHeaders !== null) {
  364. try {
  365. $onHeaders($easy->response);
  366. } catch (\Exception $e) {
  367. $easy->onHeadersException = $e;
  368. return -1;
  369. }
  370. }
  371. } elseif ($startingResponse) {
  372. $startingResponse = false;
  373. $easy->headers = [$value];
  374. } else {
  375. $easy->headers[] = $value;
  376. }
  377. return strlen($h);
  378. };
  379. }
  380. }