Client.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2016, ownCloud, Inc.
  5. *
  6. * @author Carlos Ferreira <carlos@reendex.com>
  7. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  8. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  9. * @author Joas Schilling <coding@schilljs.com>
  10. * @author Lukas Reschke <lukas@statuscode.ch>
  11. * @author Mohammed Abdellatif <m.latief@gmail.com>
  12. * @author Morris Jobke <hey@morrisjobke.de>
  13. * @author Robin Appelman <robin@icewind.nl>
  14. * @author Roeland Jago Douma <roeland@famdouma.nl>
  15. * @author Scott Shambarger <devel@shambarger.net>
  16. *
  17. * @license AGPL-3.0
  18. *
  19. * This code is free software: you can redistribute it and/or modify
  20. * it under the terms of the GNU Affero General Public License, version 3,
  21. * as published by the Free Software Foundation.
  22. *
  23. * This program is distributed in the hope that it will be useful,
  24. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  25. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  26. * GNU Affero General Public License for more details.
  27. *
  28. * You should have received a copy of the GNU Affero General Public License, version 3,
  29. * along with this program. If not, see <http://www.gnu.org/licenses/>
  30. *
  31. */
  32. namespace OC\Http\Client;
  33. use GuzzleHttp\Client as GuzzleClient;
  34. use GuzzleHttp\Promise\PromiseInterface;
  35. use GuzzleHttp\RequestOptions;
  36. use OCP\Http\Client\IClient;
  37. use OCP\Http\Client\IPromise;
  38. use OCP\Http\Client\IResponse;
  39. use OCP\Http\Client\LocalServerException;
  40. use OCP\ICertificateManager;
  41. use OCP\IConfig;
  42. use OCP\Security\IRemoteHostValidator;
  43. use Psr\Log\LoggerInterface;
  44. use function parse_url;
  45. /**
  46. * Class Client
  47. *
  48. * @package OC\Http
  49. */
  50. class Client implements IClient {
  51. /** @var GuzzleClient */
  52. private $client;
  53. /** @var IConfig */
  54. private $config;
  55. /** @var ICertificateManager */
  56. private $certificateManager;
  57. private IRemoteHostValidator $remoteHostValidator;
  58. public function __construct(
  59. IConfig $config,
  60. ICertificateManager $certificateManager,
  61. GuzzleClient $client,
  62. IRemoteHostValidator $remoteHostValidator,
  63. protected LoggerInterface $logger,
  64. ) {
  65. $this->config = $config;
  66. $this->client = $client;
  67. $this->certificateManager = $certificateManager;
  68. $this->remoteHostValidator = $remoteHostValidator;
  69. }
  70. private function buildRequestOptions(array $options): array {
  71. $proxy = $this->getProxyUri();
  72. $defaults = [
  73. RequestOptions::VERIFY => $this->getCertBundle(),
  74. RequestOptions::TIMEOUT => 30,
  75. ];
  76. $options['nextcloud']['allow_local_address'] = $this->isLocalAddressAllowed($options);
  77. if ($options['nextcloud']['allow_local_address'] === false) {
  78. $onRedirectFunction = function (
  79. \Psr\Http\Message\RequestInterface $request,
  80. \Psr\Http\Message\ResponseInterface $response,
  81. \Psr\Http\Message\UriInterface $uri
  82. ) use ($options) {
  83. $this->preventLocalAddress($uri->__toString(), $options);
  84. };
  85. $defaults[RequestOptions::ALLOW_REDIRECTS] = [
  86. 'on_redirect' => $onRedirectFunction
  87. ];
  88. }
  89. // Only add RequestOptions::PROXY if Nextcloud is explicitly
  90. // configured to use a proxy. This is needed in order not to override
  91. // Guzzle default values.
  92. if ($proxy !== null) {
  93. $defaults[RequestOptions::PROXY] = $proxy;
  94. }
  95. $options = array_merge($defaults, $options);
  96. if (!isset($options[RequestOptions::HEADERS]['User-Agent'])) {
  97. $options[RequestOptions::HEADERS]['User-Agent'] = 'Nextcloud Server Crawler';
  98. }
  99. if (!isset($options[RequestOptions::HEADERS]['Accept-Encoding'])) {
  100. $options[RequestOptions::HEADERS]['Accept-Encoding'] = 'gzip';
  101. }
  102. // Fallback for save_to
  103. if (isset($options['save_to'])) {
  104. $options['sink'] = $options['save_to'];
  105. unset($options['save_to']);
  106. }
  107. return $options;
  108. }
  109. private function getCertBundle(): string {
  110. // If the instance is not yet setup we need to use the static path as
  111. // $this->certificateManager->getAbsoluteBundlePath() tries to instantiate
  112. // a view
  113. if (!$this->config->getSystemValueBool('installed', false)) {
  114. return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
  115. }
  116. return $this->certificateManager->getAbsoluteBundlePath();
  117. }
  118. /**
  119. * Returns a null or an associative array specifying the proxy URI for
  120. * 'http' and 'https' schemes, in addition to a 'no' key value pair
  121. * providing a list of host names that should not be proxied to.
  122. *
  123. * @return array|null
  124. *
  125. * The return array looks like:
  126. * [
  127. * 'http' => 'username:password@proxy.example.com',
  128. * 'https' => 'username:password@proxy.example.com',
  129. * 'no' => ['foo.com', 'bar.com']
  130. * ]
  131. *
  132. */
  133. private function getProxyUri(): ?array {
  134. $proxyHost = $this->config->getSystemValueString('proxy', '');
  135. if ($proxyHost === '') {
  136. return null;
  137. }
  138. $proxyUserPwd = $this->config->getSystemValueString('proxyuserpwd', '');
  139. if ($proxyUserPwd !== '') {
  140. $proxyHost = $proxyUserPwd . '@' . $proxyHost;
  141. }
  142. $proxy = [
  143. 'http' => $proxyHost,
  144. 'https' => $proxyHost,
  145. ];
  146. $proxyExclude = $this->config->getSystemValue('proxyexclude', []);
  147. if ($proxyExclude !== [] && $proxyExclude !== null) {
  148. $proxy['no'] = $proxyExclude;
  149. }
  150. return $proxy;
  151. }
  152. private function isLocalAddressAllowed(array $options) : bool {
  153. if (($options['nextcloud']['allow_local_address'] ?? false) ||
  154. $this->config->getSystemValueBool('allow_local_remote_servers', false)) {
  155. return true;
  156. }
  157. return false;
  158. }
  159. protected function preventLocalAddress(string $uri, array $options): void {
  160. if ($this->isLocalAddressAllowed($options)) {
  161. return;
  162. }
  163. $host = parse_url($uri, PHP_URL_HOST);
  164. if ($host === false || $host === null) {
  165. throw new LocalServerException('Could not detect any host');
  166. }
  167. if (!$this->remoteHostValidator->isValid($host)) {
  168. throw new LocalServerException('Host violates local access rules');
  169. }
  170. }
  171. /**
  172. * Sends a GET request
  173. *
  174. * @param string $uri
  175. * @param array $options Array such as
  176. * 'query' => [
  177. * 'field' => 'abc',
  178. * 'other_field' => '123',
  179. * 'file_name' => fopen('/path/to/file', 'r'),
  180. * ],
  181. * 'headers' => [
  182. * 'foo' => 'bar',
  183. * ],
  184. * 'cookies' => [
  185. * 'foo' => 'bar',
  186. * ],
  187. * 'allow_redirects' => [
  188. * 'max' => 10, // allow at most 10 redirects.
  189. * 'strict' => true, // use "strict" RFC compliant redirects.
  190. * 'referer' => true, // add a Referer header
  191. * 'protocols' => ['https'] // only allow https URLs
  192. * ],
  193. * 'sink' => '/path/to/file', // save to a file or a stream
  194. * 'verify' => true, // bool or string to CA file
  195. * 'debug' => true,
  196. * 'timeout' => 5,
  197. * @return IResponse
  198. * @throws \Exception If the request could not get completed
  199. */
  200. public function get(string $uri, array $options = []): IResponse {
  201. $this->preventLocalAddress($uri, $options);
  202. $response = $this->client->request('get', $uri, $this->buildRequestOptions($options));
  203. $isStream = isset($options['stream']) && $options['stream'];
  204. return new Response($response, $isStream);
  205. }
  206. /**
  207. * Sends a HEAD request
  208. *
  209. * @param string $uri
  210. * @param array $options Array such as
  211. * 'headers' => [
  212. * 'foo' => 'bar',
  213. * ],
  214. * 'cookies' => [
  215. * 'foo' => 'bar',
  216. * ],
  217. * 'allow_redirects' => [
  218. * 'max' => 10, // allow at most 10 redirects.
  219. * 'strict' => true, // use "strict" RFC compliant redirects.
  220. * 'referer' => true, // add a Referer header
  221. * 'protocols' => ['https'] // only allow https URLs
  222. * ],
  223. * 'sink' => '/path/to/file', // save to a file or a stream
  224. * 'verify' => true, // bool or string to CA file
  225. * 'debug' => true,
  226. * 'timeout' => 5,
  227. * @return IResponse
  228. * @throws \Exception If the request could not get completed
  229. */
  230. public function head(string $uri, array $options = []): IResponse {
  231. $this->preventLocalAddress($uri, $options);
  232. $response = $this->client->request('head', $uri, $this->buildRequestOptions($options));
  233. return new Response($response);
  234. }
  235. /**
  236. * Sends a POST request
  237. *
  238. * @param string $uri
  239. * @param array $options Array such as
  240. * 'body' => [
  241. * 'field' => 'abc',
  242. * 'other_field' => '123',
  243. * 'file_name' => fopen('/path/to/file', 'r'),
  244. * ],
  245. * 'headers' => [
  246. * 'foo' => 'bar',
  247. * ],
  248. * 'cookies' => [
  249. * 'foo' => 'bar',
  250. * ],
  251. * 'allow_redirects' => [
  252. * 'max' => 10, // allow at most 10 redirects.
  253. * 'strict' => true, // use "strict" RFC compliant redirects.
  254. * 'referer' => true, // add a Referer header
  255. * 'protocols' => ['https'] // only allow https URLs
  256. * ],
  257. * 'sink' => '/path/to/file', // save to a file or a stream
  258. * 'verify' => true, // bool or string to CA file
  259. * 'debug' => true,
  260. * 'timeout' => 5,
  261. * @return IResponse
  262. * @throws \Exception If the request could not get completed
  263. */
  264. public function post(string $uri, array $options = []): IResponse {
  265. $this->preventLocalAddress($uri, $options);
  266. if (isset($options['body']) && is_array($options['body'])) {
  267. $options['form_params'] = $options['body'];
  268. unset($options['body']);
  269. }
  270. $response = $this->client->request('post', $uri, $this->buildRequestOptions($options));
  271. $isStream = isset($options['stream']) && $options['stream'];
  272. return new Response($response, $isStream);
  273. }
  274. /**
  275. * Sends a PUT request
  276. *
  277. * @param string $uri
  278. * @param array $options Array such as
  279. * 'body' => [
  280. * 'field' => 'abc',
  281. * 'other_field' => '123',
  282. * 'file_name' => fopen('/path/to/file', 'r'),
  283. * ],
  284. * 'headers' => [
  285. * 'foo' => 'bar',
  286. * ],
  287. * 'cookies' => [
  288. * 'foo' => 'bar',
  289. * ],
  290. * 'allow_redirects' => [
  291. * 'max' => 10, // allow at most 10 redirects.
  292. * 'strict' => true, // use "strict" RFC compliant redirects.
  293. * 'referer' => true, // add a Referer header
  294. * 'protocols' => ['https'] // only allow https URLs
  295. * ],
  296. * 'sink' => '/path/to/file', // save to a file or a stream
  297. * 'verify' => true, // bool or string to CA file
  298. * 'debug' => true,
  299. * 'timeout' => 5,
  300. * @return IResponse
  301. * @throws \Exception If the request could not get completed
  302. */
  303. public function put(string $uri, array $options = []): IResponse {
  304. $this->preventLocalAddress($uri, $options);
  305. $response = $this->client->request('put', $uri, $this->buildRequestOptions($options));
  306. return new Response($response);
  307. }
  308. /**
  309. * Sends a DELETE request
  310. *
  311. * @param string $uri
  312. * @param array $options Array such as
  313. * 'body' => [
  314. * 'field' => 'abc',
  315. * 'other_field' => '123',
  316. * 'file_name' => fopen('/path/to/file', 'r'),
  317. * ],
  318. * 'headers' => [
  319. * 'foo' => 'bar',
  320. * ],
  321. * 'cookies' => [
  322. * 'foo' => 'bar',
  323. * ],
  324. * 'allow_redirects' => [
  325. * 'max' => 10, // allow at most 10 redirects.
  326. * 'strict' => true, // use "strict" RFC compliant redirects.
  327. * 'referer' => true, // add a Referer header
  328. * 'protocols' => ['https'] // only allow https URLs
  329. * ],
  330. * 'sink' => '/path/to/file', // save to a file or a stream
  331. * 'verify' => true, // bool or string to CA file
  332. * 'debug' => true,
  333. * 'timeout' => 5,
  334. * @return IResponse
  335. * @throws \Exception If the request could not get completed
  336. */
  337. public function delete(string $uri, array $options = []): IResponse {
  338. $this->preventLocalAddress($uri, $options);
  339. $response = $this->client->request('delete', $uri, $this->buildRequestOptions($options));
  340. return new Response($response);
  341. }
  342. /**
  343. * Sends an OPTIONS request
  344. *
  345. * @param string $uri
  346. * @param array $options Array such as
  347. * 'body' => [
  348. * 'field' => 'abc',
  349. * 'other_field' => '123',
  350. * 'file_name' => fopen('/path/to/file', 'r'),
  351. * ],
  352. * 'headers' => [
  353. * 'foo' => 'bar',
  354. * ],
  355. * 'cookies' => [
  356. * 'foo' => 'bar',
  357. * ],
  358. * 'allow_redirects' => [
  359. * 'max' => 10, // allow at most 10 redirects.
  360. * 'strict' => true, // use "strict" RFC compliant redirects.
  361. * 'referer' => true, // add a Referer header
  362. * 'protocols' => ['https'] // only allow https URLs
  363. * ],
  364. * 'sink' => '/path/to/file', // save to a file or a stream
  365. * 'verify' => true, // bool or string to CA file
  366. * 'debug' => true,
  367. * 'timeout' => 5,
  368. * @return IResponse
  369. * @throws \Exception If the request could not get completed
  370. */
  371. public function options(string $uri, array $options = []): IResponse {
  372. $this->preventLocalAddress($uri, $options);
  373. $response = $this->client->request('options', $uri, $this->buildRequestOptions($options));
  374. return new Response($response);
  375. }
  376. protected function wrapGuzzlePromise(PromiseInterface $promise): IPromise {
  377. return new GuzzlePromiseAdapter(
  378. $promise,
  379. $this->logger
  380. );
  381. }
  382. /**
  383. * Sends an asynchronous GET request
  384. *
  385. * @param string $uri
  386. * @param array $options Array such as
  387. * 'query' => [
  388. * 'field' => 'abc',
  389. * 'other_field' => '123',
  390. * 'file_name' => fopen('/path/to/file', 'r'),
  391. * ],
  392. * 'headers' => [
  393. * 'foo' => 'bar',
  394. * ],
  395. * 'cookies' => [
  396. * 'foo' => 'bar',
  397. * ],
  398. * 'allow_redirects' => [
  399. * 'max' => 10, // allow at most 10 redirects.
  400. * 'strict' => true, // use "strict" RFC compliant redirects.
  401. * 'referer' => true, // add a Referer header
  402. * 'protocols' => ['https'] // only allow https URLs
  403. * ],
  404. * 'sink' => '/path/to/file', // save to a file or a stream
  405. * 'verify' => true, // bool or string to CA file
  406. * 'debug' => true,
  407. * 'timeout' => 5,
  408. * @return IPromise
  409. */
  410. public function getAsync(string $uri, array $options = []): IPromise {
  411. $this->preventLocalAddress($uri, $options);
  412. $response = $this->client->requestAsync('get', $uri, $this->buildRequestOptions($options));
  413. return $this->wrapGuzzlePromise($response);
  414. }
  415. /**
  416. * Sends an asynchronous HEAD request
  417. *
  418. * @param string $uri
  419. * @param array $options Array such as
  420. * 'headers' => [
  421. * 'foo' => 'bar',
  422. * ],
  423. * 'cookies' => [
  424. * 'foo' => 'bar',
  425. * ],
  426. * 'allow_redirects' => [
  427. * 'max' => 10, // allow at most 10 redirects.
  428. * 'strict' => true, // use "strict" RFC compliant redirects.
  429. * 'referer' => true, // add a Referer header
  430. * 'protocols' => ['https'] // only allow https URLs
  431. * ],
  432. * 'sink' => '/path/to/file', // save to a file or a stream
  433. * 'verify' => true, // bool or string to CA file
  434. * 'debug' => true,
  435. * 'timeout' => 5,
  436. * @return IPromise
  437. */
  438. public function headAsync(string $uri, array $options = []): IPromise {
  439. $this->preventLocalAddress($uri, $options);
  440. $response = $this->client->requestAsync('head', $uri, $this->buildRequestOptions($options));
  441. return $this->wrapGuzzlePromise($response);
  442. }
  443. /**
  444. * Sends an asynchronous POST request
  445. *
  446. * @param string $uri
  447. * @param array $options Array such as
  448. * 'body' => [
  449. * 'field' => 'abc',
  450. * 'other_field' => '123',
  451. * 'file_name' => fopen('/path/to/file', 'r'),
  452. * ],
  453. * 'headers' => [
  454. * 'foo' => 'bar',
  455. * ],
  456. * 'cookies' => [
  457. * 'foo' => 'bar',
  458. * ],
  459. * 'allow_redirects' => [
  460. * 'max' => 10, // allow at most 10 redirects.
  461. * 'strict' => true, // use "strict" RFC compliant redirects.
  462. * 'referer' => true, // add a Referer header
  463. * 'protocols' => ['https'] // only allow https URLs
  464. * ],
  465. * 'sink' => '/path/to/file', // save to a file or a stream
  466. * 'verify' => true, // bool or string to CA file
  467. * 'debug' => true,
  468. * 'timeout' => 5,
  469. * @return IPromise
  470. */
  471. public function postAsync(string $uri, array $options = []): IPromise {
  472. $this->preventLocalAddress($uri, $options);
  473. if (isset($options['body']) && is_array($options['body'])) {
  474. $options['form_params'] = $options['body'];
  475. unset($options['body']);
  476. }
  477. return $this->wrapGuzzlePromise($this->client->requestAsync('post', $uri, $this->buildRequestOptions($options)));
  478. }
  479. /**
  480. * Sends an asynchronous PUT request
  481. *
  482. * @param string $uri
  483. * @param array $options Array such as
  484. * 'body' => [
  485. * 'field' => 'abc',
  486. * 'other_field' => '123',
  487. * 'file_name' => fopen('/path/to/file', 'r'),
  488. * ],
  489. * 'headers' => [
  490. * 'foo' => 'bar',
  491. * ],
  492. * 'cookies' => [
  493. * 'foo' => 'bar',
  494. * ],
  495. * 'allow_redirects' => [
  496. * 'max' => 10, // allow at most 10 redirects.
  497. * 'strict' => true, // use "strict" RFC compliant redirects.
  498. * 'referer' => true, // add a Referer header
  499. * 'protocols' => ['https'] // only allow https URLs
  500. * ],
  501. * 'sink' => '/path/to/file', // save to a file or a stream
  502. * 'verify' => true, // bool or string to CA file
  503. * 'debug' => true,
  504. * 'timeout' => 5,
  505. * @return IPromise
  506. */
  507. public function putAsync(string $uri, array $options = []): IPromise {
  508. $this->preventLocalAddress($uri, $options);
  509. $response = $this->client->requestAsync('put', $uri, $this->buildRequestOptions($options));
  510. return $this->wrapGuzzlePromise($response);
  511. }
  512. /**
  513. * Sends an asynchronous DELETE request
  514. *
  515. * @param string $uri
  516. * @param array $options Array such as
  517. * 'body' => [
  518. * 'field' => 'abc',
  519. * 'other_field' => '123',
  520. * 'file_name' => fopen('/path/to/file', 'r'),
  521. * ],
  522. * 'headers' => [
  523. * 'foo' => 'bar',
  524. * ],
  525. * 'cookies' => [
  526. * 'foo' => 'bar',
  527. * ],
  528. * 'allow_redirects' => [
  529. * 'max' => 10, // allow at most 10 redirects.
  530. * 'strict' => true, // use "strict" RFC compliant redirects.
  531. * 'referer' => true, // add a Referer header
  532. * 'protocols' => ['https'] // only allow https URLs
  533. * ],
  534. * 'sink' => '/path/to/file', // save to a file or a stream
  535. * 'verify' => true, // bool or string to CA file
  536. * 'debug' => true,
  537. * 'timeout' => 5,
  538. * @return IPromise
  539. */
  540. public function deleteAsync(string $uri, array $options = []): IPromise {
  541. $this->preventLocalAddress($uri, $options);
  542. $response = $this->client->requestAsync('delete', $uri, $this->buildRequestOptions($options));
  543. return $this->wrapGuzzlePromise($response);
  544. }
  545. /**
  546. * Sends an asynchronous OPTIONS request
  547. *
  548. * @param string $uri
  549. * @param array $options Array such as
  550. * 'body' => [
  551. * 'field' => 'abc',
  552. * 'other_field' => '123',
  553. * 'file_name' => fopen('/path/to/file', 'r'),
  554. * ],
  555. * 'headers' => [
  556. * 'foo' => 'bar',
  557. * ],
  558. * 'cookies' => [
  559. * 'foo' => 'bar',
  560. * ],
  561. * 'allow_redirects' => [
  562. * 'max' => 10, // allow at most 10 redirects.
  563. * 'strict' => true, // use "strict" RFC compliant redirects.
  564. * 'referer' => true, // add a Referer header
  565. * 'protocols' => ['https'] // only allow https URLs
  566. * ],
  567. * 'sink' => '/path/to/file', // save to a file or a stream
  568. * 'verify' => true, // bool or string to CA file
  569. * 'debug' => true,
  570. * 'timeout' => 5,
  571. * @return IPromise
  572. */
  573. public function optionsAsync(string $uri, array $options = []): IPromise {
  574. $this->preventLocalAddress($uri, $options);
  575. $response = $this->client->requestAsync('options', $uri, $this->buildRequestOptions($options));
  576. return $this->wrapGuzzlePromise($response);
  577. }
  578. }