Client.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  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\RequestOptions;
  35. use OCP\Http\Client\IClient;
  36. use OCP\Http\Client\IResponse;
  37. use OCP\Http\Client\LocalServerException;
  38. use OCP\ICertificateManager;
  39. use OCP\IConfig;
  40. use OCP\Security\IRemoteHostValidator;
  41. use function parse_url;
  42. /**
  43. * Class Client
  44. *
  45. * @package OC\Http
  46. */
  47. class Client implements IClient {
  48. /** @var GuzzleClient */
  49. private $client;
  50. /** @var IConfig */
  51. private $config;
  52. /** @var ICertificateManager */
  53. private $certificateManager;
  54. private IRemoteHostValidator $remoteHostValidator;
  55. public function __construct(
  56. IConfig $config,
  57. ICertificateManager $certificateManager,
  58. GuzzleClient $client,
  59. IRemoteHostValidator $remoteHostValidator
  60. ) {
  61. $this->config = $config;
  62. $this->client = $client;
  63. $this->certificateManager = $certificateManager;
  64. $this->remoteHostValidator = $remoteHostValidator;
  65. }
  66. private function buildRequestOptions(array $options): array {
  67. $proxy = $this->getProxyUri();
  68. $defaults = [
  69. RequestOptions::VERIFY => $this->getCertBundle(),
  70. RequestOptions::TIMEOUT => 30,
  71. ];
  72. $options['nextcloud']['allow_local_address'] = $this->isLocalAddressAllowed($options);
  73. if ($options['nextcloud']['allow_local_address'] === false) {
  74. $onRedirectFunction = function (
  75. \Psr\Http\Message\RequestInterface $request,
  76. \Psr\Http\Message\ResponseInterface $response,
  77. \Psr\Http\Message\UriInterface $uri
  78. ) use ($options) {
  79. $this->preventLocalAddress($uri->__toString(), $options);
  80. };
  81. $defaults[RequestOptions::ALLOW_REDIRECTS] = [
  82. 'on_redirect' => $onRedirectFunction
  83. ];
  84. }
  85. // Only add RequestOptions::PROXY if Nextcloud is explicitly
  86. // configured to use a proxy. This is needed in order not to override
  87. // Guzzle default values.
  88. if ($proxy !== null) {
  89. $defaults[RequestOptions::PROXY] = $proxy;
  90. }
  91. $options = array_merge($defaults, $options);
  92. if (!isset($options[RequestOptions::HEADERS]['User-Agent'])) {
  93. $options[RequestOptions::HEADERS]['User-Agent'] = 'Nextcloud Server Crawler';
  94. }
  95. if (!isset($options[RequestOptions::HEADERS]['Accept-Encoding'])) {
  96. $options[RequestOptions::HEADERS]['Accept-Encoding'] = 'gzip';
  97. }
  98. // Fallback for save_to
  99. if (isset($options['save_to'])) {
  100. $options['sink'] = $options['save_to'];
  101. unset($options['save_to']);
  102. }
  103. return $options;
  104. }
  105. private function getCertBundle(): string {
  106. // If the instance is not yet setup we need to use the static path as
  107. // $this->certificateManager->getAbsoluteBundlePath() tries to instantiate
  108. // a view
  109. if ($this->config->getSystemValue('installed', false) === false) {
  110. return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
  111. }
  112. return $this->certificateManager->getAbsoluteBundlePath();
  113. }
  114. /**
  115. * Returns a null or an associative array specifying the proxy URI for
  116. * 'http' and 'https' schemes, in addition to a 'no' key value pair
  117. * providing a list of host names that should not be proxied to.
  118. *
  119. * @return array|null
  120. *
  121. * The return array looks like:
  122. * [
  123. * 'http' => 'username:password@proxy.example.com',
  124. * 'https' => 'username:password@proxy.example.com',
  125. * 'no' => ['foo.com', 'bar.com']
  126. * ]
  127. *
  128. */
  129. private function getProxyUri(): ?array {
  130. $proxyHost = $this->config->getSystemValue('proxy', '');
  131. if ($proxyHost === '' || $proxyHost === null) {
  132. return null;
  133. }
  134. $proxyUserPwd = $this->config->getSystemValue('proxyuserpwd', '');
  135. if ($proxyUserPwd !== '' && $proxyUserPwd !== null) {
  136. $proxyHost = $proxyUserPwd . '@' . $proxyHost;
  137. }
  138. $proxy = [
  139. 'http' => $proxyHost,
  140. 'https' => $proxyHost,
  141. ];
  142. $proxyExclude = $this->config->getSystemValue('proxyexclude', []);
  143. if ($proxyExclude !== [] && $proxyExclude !== null) {
  144. $proxy['no'] = $proxyExclude;
  145. }
  146. return $proxy;
  147. }
  148. private function isLocalAddressAllowed(array $options) : bool {
  149. if (($options['nextcloud']['allow_local_address'] ?? false) ||
  150. $this->config->getSystemValueBool('allow_local_remote_servers', false)) {
  151. return true;
  152. }
  153. return false;
  154. }
  155. protected function preventLocalAddress(string $uri, array $options): void {
  156. if ($this->isLocalAddressAllowed($options)) {
  157. return;
  158. }
  159. $host = parse_url($uri, PHP_URL_HOST);
  160. if ($host === false || $host === null) {
  161. throw new LocalServerException('Could not detect any host');
  162. }
  163. if (!$this->remoteHostValidator->isValid($host)) {
  164. throw new LocalServerException('Host violates local access rules');
  165. }
  166. }
  167. /**
  168. * Sends a GET request
  169. *
  170. * @param string $uri
  171. * @param array $options Array such as
  172. * 'query' => [
  173. * 'field' => 'abc',
  174. * 'other_field' => '123',
  175. * 'file_name' => fopen('/path/to/file', 'r'),
  176. * ],
  177. * 'headers' => [
  178. * 'foo' => 'bar',
  179. * ],
  180. * 'cookies' => ['
  181. * 'foo' => 'bar',
  182. * ],
  183. * 'allow_redirects' => [
  184. * 'max' => 10, // allow at most 10 redirects.
  185. * 'strict' => true, // use "strict" RFC compliant redirects.
  186. * 'referer' => true, // add a Referer header
  187. * 'protocols' => ['https'] // only allow https URLs
  188. * ],
  189. * 'sink' => '/path/to/file', // save to a file or a stream
  190. * 'verify' => true, // bool or string to CA file
  191. * 'debug' => true,
  192. * 'timeout' => 5,
  193. * @return IResponse
  194. * @throws \Exception If the request could not get completed
  195. */
  196. public function get(string $uri, array $options = []): IResponse {
  197. $this->preventLocalAddress($uri, $options);
  198. $response = $this->client->request('get', $uri, $this->buildRequestOptions($options));
  199. $isStream = isset($options['stream']) && $options['stream'];
  200. return new Response($response, $isStream);
  201. }
  202. /**
  203. * Sends a HEAD request
  204. *
  205. * @param string $uri
  206. * @param array $options Array such as
  207. * 'headers' => [
  208. * 'foo' => 'bar',
  209. * ],
  210. * 'cookies' => ['
  211. * 'foo' => 'bar',
  212. * ],
  213. * 'allow_redirects' => [
  214. * 'max' => 10, // allow at most 10 redirects.
  215. * 'strict' => true, // use "strict" RFC compliant redirects.
  216. * 'referer' => true, // add a Referer header
  217. * 'protocols' => ['https'] // only allow https URLs
  218. * ],
  219. * 'sink' => '/path/to/file', // save to a file or a stream
  220. * 'verify' => true, // bool or string to CA file
  221. * 'debug' => true,
  222. * 'timeout' => 5,
  223. * @return IResponse
  224. * @throws \Exception If the request could not get completed
  225. */
  226. public function head(string $uri, array $options = []): IResponse {
  227. $this->preventLocalAddress($uri, $options);
  228. $response = $this->client->request('head', $uri, $this->buildRequestOptions($options));
  229. return new Response($response);
  230. }
  231. /**
  232. * Sends a POST request
  233. *
  234. * @param string $uri
  235. * @param array $options Array such as
  236. * 'body' => [
  237. * 'field' => 'abc',
  238. * 'other_field' => '123',
  239. * 'file_name' => fopen('/path/to/file', 'r'),
  240. * ],
  241. * 'headers' => [
  242. * 'foo' => 'bar',
  243. * ],
  244. * 'cookies' => ['
  245. * 'foo' => 'bar',
  246. * ],
  247. * 'allow_redirects' => [
  248. * 'max' => 10, // allow at most 10 redirects.
  249. * 'strict' => true, // use "strict" RFC compliant redirects.
  250. * 'referer' => true, // add a Referer header
  251. * 'protocols' => ['https'] // only allow https URLs
  252. * ],
  253. * 'sink' => '/path/to/file', // save to a file or a stream
  254. * 'verify' => true, // bool or string to CA file
  255. * 'debug' => true,
  256. * 'timeout' => 5,
  257. * @return IResponse
  258. * @throws \Exception If the request could not get completed
  259. */
  260. public function post(string $uri, array $options = []): IResponse {
  261. $this->preventLocalAddress($uri, $options);
  262. if (isset($options['body']) && is_array($options['body'])) {
  263. $options['form_params'] = $options['body'];
  264. unset($options['body']);
  265. }
  266. $response = $this->client->request('post', $uri, $this->buildRequestOptions($options));
  267. $isStream = isset($options['stream']) && $options['stream'];
  268. return new Response($response, $isStream);
  269. }
  270. /**
  271. * Sends a PUT request
  272. *
  273. * @param string $uri
  274. * @param array $options Array such as
  275. * 'body' => [
  276. * 'field' => 'abc',
  277. * 'other_field' => '123',
  278. * 'file_name' => fopen('/path/to/file', 'r'),
  279. * ],
  280. * 'headers' => [
  281. * 'foo' => 'bar',
  282. * ],
  283. * 'cookies' => ['
  284. * 'foo' => 'bar',
  285. * ],
  286. * 'allow_redirects' => [
  287. * 'max' => 10, // allow at most 10 redirects.
  288. * 'strict' => true, // use "strict" RFC compliant redirects.
  289. * 'referer' => true, // add a Referer header
  290. * 'protocols' => ['https'] // only allow https URLs
  291. * ],
  292. * 'sink' => '/path/to/file', // save to a file or a stream
  293. * 'verify' => true, // bool or string to CA file
  294. * 'debug' => true,
  295. * 'timeout' => 5,
  296. * @return IResponse
  297. * @throws \Exception If the request could not get completed
  298. */
  299. public function put(string $uri, array $options = []): IResponse {
  300. $this->preventLocalAddress($uri, $options);
  301. $response = $this->client->request('put', $uri, $this->buildRequestOptions($options));
  302. return new Response($response);
  303. }
  304. /**
  305. * Sends a DELETE request
  306. *
  307. * @param string $uri
  308. * @param array $options Array such as
  309. * 'body' => [
  310. * 'field' => 'abc',
  311. * 'other_field' => '123',
  312. * 'file_name' => fopen('/path/to/file', 'r'),
  313. * ],
  314. * 'headers' => [
  315. * 'foo' => 'bar',
  316. * ],
  317. * 'cookies' => ['
  318. * 'foo' => 'bar',
  319. * ],
  320. * 'allow_redirects' => [
  321. * 'max' => 10, // allow at most 10 redirects.
  322. * 'strict' => true, // use "strict" RFC compliant redirects.
  323. * 'referer' => true, // add a Referer header
  324. * 'protocols' => ['https'] // only allow https URLs
  325. * ],
  326. * 'sink' => '/path/to/file', // save to a file or a stream
  327. * 'verify' => true, // bool or string to CA file
  328. * 'debug' => true,
  329. * 'timeout' => 5,
  330. * @return IResponse
  331. * @throws \Exception If the request could not get completed
  332. */
  333. public function delete(string $uri, array $options = []): IResponse {
  334. $this->preventLocalAddress($uri, $options);
  335. $response = $this->client->request('delete', $uri, $this->buildRequestOptions($options));
  336. return new Response($response);
  337. }
  338. /**
  339. * Sends a options request
  340. *
  341. * @param string $uri
  342. * @param array $options Array such as
  343. * 'body' => [
  344. * 'field' => 'abc',
  345. * 'other_field' => '123',
  346. * 'file_name' => fopen('/path/to/file', 'r'),
  347. * ],
  348. * 'headers' => [
  349. * 'foo' => 'bar',
  350. * ],
  351. * 'cookies' => ['
  352. * 'foo' => 'bar',
  353. * ],
  354. * 'allow_redirects' => [
  355. * 'max' => 10, // allow at most 10 redirects.
  356. * 'strict' => true, // use "strict" RFC compliant redirects.
  357. * 'referer' => true, // add a Referer header
  358. * 'protocols' => ['https'] // only allow https URLs
  359. * ],
  360. * 'sink' => '/path/to/file', // save to a file or a stream
  361. * 'verify' => true, // bool or string to CA file
  362. * 'debug' => true,
  363. * 'timeout' => 5,
  364. * @return IResponse
  365. * @throws \Exception If the request could not get completed
  366. */
  367. public function options(string $uri, array $options = []): IResponse {
  368. $this->preventLocalAddress($uri, $options);
  369. $response = $this->client->request('options', $uri, $this->buildRequestOptions($options));
  370. return new Response($response);
  371. }
  372. }