You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

412 lines
14 KiB

3 years ago
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2016, ownCloud, Inc.
  5. *
  6. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  7. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  8. * @author Joas Schilling <coding@schilljs.com>
  9. * @author Lukas Reschke <lukas@statuscode.ch>
  10. * @author Mohammed Abdellatif <m.latief@gmail.com>
  11. * @author Morris Jobke <hey@morrisjobke.de>
  12. * @author Robin Appelman <robin@icewind.nl>
  13. * @author Roeland Jago Douma <roeland@famdouma.nl>
  14. * @author Scott Shambarger <devel@shambarger.net>
  15. *
  16. * @license AGPL-3.0
  17. *
  18. * This code is free software: you can redistribute it and/or modify
  19. * it under the terms of the GNU Affero General Public License, version 3,
  20. * as published by the Free Software Foundation.
  21. *
  22. * This program is distributed in the hope that it will be useful,
  23. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  24. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  25. * GNU Affero General Public License for more details.
  26. *
  27. * You should have received a copy of the GNU Affero General Public License, version 3,
  28. * along with this program. If not, see <http://www.gnu.org/licenses/>
  29. *
  30. */
  31. namespace OC\Http\Client;
  32. use GuzzleHttp\Client as GuzzleClient;
  33. use GuzzleHttp\RequestOptions;
  34. use OCP\Http\Client\IClient;
  35. use OCP\Http\Client\IResponse;
  36. use OCP\Http\Client\LocalServerException;
  37. use OCP\ICertificateManager;
  38. use OCP\IConfig;
  39. use OCP\ILogger;
  40. /**
  41. * Class Client
  42. *
  43. * @package OC\Http
  44. */
  45. class Client implements IClient {
  46. /** @var GuzzleClient */
  47. private $client;
  48. /** @var IConfig */
  49. private $config;
  50. /** @var ILogger */
  51. private $logger;
  52. /** @var ICertificateManager */
  53. private $certificateManager;
  54. public function __construct(
  55. IConfig $config,
  56. ILogger $logger,
  57. ICertificateManager $certificateManager,
  58. GuzzleClient $client
  59. ) {
  60. $this->config = $config;
  61. $this->logger = $logger;
  62. $this->client = $client;
  63. $this->certificateManager = $certificateManager;
  64. }
  65. private function buildRequestOptions(array $options): array {
  66. $proxy = $this->getProxyUri();
  67. $defaults = [
  68. RequestOptions::VERIFY => $this->getCertBundle(),
  69. RequestOptions::TIMEOUT => 30,
  70. ];
  71. // Only add RequestOptions::PROXY if Nextcloud is explicitly
  72. // configured to use a proxy. This is needed in order not to override
  73. // Guzzle default values.
  74. if ($proxy !== null) {
  75. $defaults[RequestOptions::PROXY] = $proxy;
  76. }
  77. $options = array_merge($defaults, $options);
  78. if (!isset($options[RequestOptions::HEADERS]['User-Agent'])) {
  79. $options[RequestOptions::HEADERS]['User-Agent'] = 'Nextcloud Server Crawler';
  80. }
  81. if (!isset($options[RequestOptions::HEADERS]['Accept-Encoding'])) {
  82. $options[RequestOptions::HEADERS]['Accept-Encoding'] = 'gzip';
  83. }
  84. return $options;
  85. }
  86. private function getCertBundle(): string {
  87. // If the instance is not yet setup we need to use the static path as
  88. // $this->certificateManager->getAbsoluteBundlePath() tries to instantiiate
  89. // a view
  90. if ($this->config->getSystemValue('installed', false) === false) {
  91. return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
  92. }
  93. if ($this->certificateManager->listCertificates() === []) {
  94. return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
  95. }
  96. return $this->certificateManager->getAbsoluteBundlePath();
  97. }
  98. /**
  99. * Returns a null or an associative array specifiying the proxy URI for
  100. * 'http' and 'https' schemes, in addition to a 'no' key value pair
  101. * providing a list of host names that should not be proxied to.
  102. *
  103. * @return array|null
  104. *
  105. * The return array looks like:
  106. * [
  107. * 'http' => 'username:password@proxy.example.com',
  108. * 'https' => 'username:password@proxy.example.com',
  109. * 'no' => ['foo.com', 'bar.com']
  110. * ]
  111. *
  112. */
  113. private function getProxyUri(): ?array {
  114. $proxyHost = $this->config->getSystemValue('proxy', '');
  115. if ($proxyHost === '' || $proxyHost === null) {
  116. return null;
  117. }
  118. $proxyUserPwd = $this->config->getSystemValue('proxyuserpwd', '');
  119. if ($proxyUserPwd !== '' && $proxyUserPwd !== null) {
  120. $proxyHost = $proxyUserPwd . '@' . $proxyHost;
  121. }
  122. $proxy = [
  123. 'http' => $proxyHost,
  124. 'https' => $proxyHost,
  125. ];
  126. $proxyExclude = $this->config->getSystemValue('proxyexclude', []);
  127. if ($proxyExclude !== [] && $proxyExclude !== null) {
  128. $proxy['no'] = $proxyExclude;
  129. }
  130. return $proxy;
  131. }
  132. protected function preventLocalAddress(string $uri, array $options): void {
  133. if (($options['nextcloud']['allow_local_address'] ?? false) ||
  134. $this->config->getSystemValueBool('allow_local_remote_servers', false)) {
  135. return;
  136. }
  137. $host = parse_url($uri, PHP_URL_HOST);
  138. if ($host === false || $host === null) {
  139. $this->logger->warning("Could not detect any host in $uri");
  140. throw new LocalServerException('Could not detect any host');
  141. }
  142. $host = strtolower($host);
  143. // remove brackets from IPv6 addresses
  144. if (strpos($host, '[') === 0 && substr($host, -1) === ']') {
  145. $host = substr($host, 1, -1);
  146. }
  147. // Disallow localhost and local network
  148. if ($host === 'localhost' || substr($host, -6) === '.local' || substr($host, -10) === '.localhost') {
  149. $this->logger->warning("Host $host was not connected to because it violates local access rules");
  150. throw new LocalServerException('Host violates local access rules');
  151. }
  152. // Disallow hostname only
  153. if (substr_count($host, '.') === 0) {
  154. $this->logger->warning("Host $host was not connected to because it violates local access rules");
  155. throw new LocalServerException('Host violates local access rules');
  156. }
  157. if ((bool)filter_var($host, FILTER_VALIDATE_IP) && !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
  158. $this->logger->warning("Host $host was not connected to because it violates local access rules");
  159. throw new LocalServerException('Host violates local access rules');
  160. }
  161. // Also check for IPv6 IPv4 nesting, because that's not covered by filter_var
  162. if ((bool)filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && substr_count($host, '.') > 0) {
  163. $delimiter = strrpos($host, ':'); // Get last colon
  164. $ipv4Address = substr($host, $delimiter + 1);
  165. if (!filter_var($ipv4Address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
  166. $this->logger->warning("Host $host was not connected to because it violates local access rules");
  167. throw new LocalServerException('Host violates local access rules');
  168. }
  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. * 'save_to' => '/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. * 'save_to' => '/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. * 'save_to' => '/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. return new Response($response);
  272. }
  273. /**
  274. * Sends a PUT request
  275. *
  276. * @param string $uri
  277. * @param array $options Array such as
  278. * 'body' => [
  279. * 'field' => 'abc',
  280. * 'other_field' => '123',
  281. * 'file_name' => fopen('/path/to/file', 'r'),
  282. * ],
  283. * 'headers' => [
  284. * 'foo' => 'bar',
  285. * ],
  286. * 'cookies' => ['
  287. * 'foo' => 'bar',
  288. * ],
  289. * 'allow_redirects' => [
  290. * 'max' => 10, // allow at most 10 redirects.
  291. * 'strict' => true, // use "strict" RFC compliant redirects.
  292. * 'referer' => true, // add a Referer header
  293. * 'protocols' => ['https'] // only allow https URLs
  294. * ],
  295. * 'save_to' => '/path/to/file', // save to a file or a stream
  296. * 'verify' => true, // bool or string to CA file
  297. * 'debug' => true,
  298. * 'timeout' => 5,
  299. * @return IResponse
  300. * @throws \Exception If the request could not get completed
  301. */
  302. public function put(string $uri, array $options = []): IResponse {
  303. $this->preventLocalAddress($uri, $options);
  304. $response = $this->client->request('put', $uri, $this->buildRequestOptions($options));
  305. return new Response($response);
  306. }
  307. /**
  308. * Sends a DELETE request
  309. *
  310. * @param string $uri
  311. * @param array $options Array such as
  312. * 'body' => [
  313. * 'field' => 'abc',
  314. * 'other_field' => '123',
  315. * 'file_name' => fopen('/path/to/file', 'r'),
  316. * ],
  317. * 'headers' => [
  318. * 'foo' => 'bar',
  319. * ],
  320. * 'cookies' => ['
  321. * 'foo' => 'bar',
  322. * ],
  323. * 'allow_redirects' => [
  324. * 'max' => 10, // allow at most 10 redirects.
  325. * 'strict' => true, // use "strict" RFC compliant redirects.
  326. * 'referer' => true, // add a Referer header
  327. * 'protocols' => ['https'] // only allow https URLs
  328. * ],
  329. * 'save_to' => '/path/to/file', // save to a file or a stream
  330. * 'verify' => true, // bool or string to CA file
  331. * 'debug' => true,
  332. * 'timeout' => 5,
  333. * @return IResponse
  334. * @throws \Exception If the request could not get completed
  335. */
  336. public function delete(string $uri, array $options = []): IResponse {
  337. $this->preventLocalAddress($uri, $options);
  338. $response = $this->client->request('delete', $uri, $this->buildRequestOptions($options));
  339. return new Response($response);
  340. }
  341. /**
  342. * Sends a options request
  343. *
  344. * @param string $uri
  345. * @param array $options Array such as
  346. * 'body' => [
  347. * 'field' => 'abc',
  348. * 'other_field' => '123',
  349. * 'file_name' => fopen('/path/to/file', 'r'),
  350. * ],
  351. * 'headers' => [
  352. * 'foo' => 'bar',
  353. * ],
  354. * 'cookies' => ['
  355. * 'foo' => 'bar',
  356. * ],
  357. * 'allow_redirects' => [
  358. * 'max' => 10, // allow at most 10 redirects.
  359. * 'strict' => true, // use "strict" RFC compliant redirects.
  360. * 'referer' => true, // add a Referer header
  361. * 'protocols' => ['https'] // only allow https URLs
  362. * ],
  363. * 'save_to' => '/path/to/file', // save to a file or a stream
  364. * 'verify' => true, // bool or string to CA file
  365. * 'debug' => true,
  366. * 'timeout' => 5,
  367. * @return IResponse
  368. * @throws \Exception If the request could not get completed
  369. */
  370. public function options(string $uri, array $options = []): IResponse {
  371. $this->preventLocalAddress($uri, $options);
  372. $response = $this->client->request('options', $uri, $this->buildRequestOptions($options));
  373. return new Response($response);
  374. }
  375. }