ClientTest.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  6. * SPDX-License-Identifier: AGPL-3.0-or-later
  7. */
  8. namespace Test\Http\Client;
  9. use GuzzleHttp\Psr7\Response;
  10. use OC\Http\Client\Client;
  11. use OC\Security\CertificateManager;
  12. use OCP\Http\Client\LocalServerException;
  13. use OCP\ICertificateManager;
  14. use OCP\IConfig;
  15. use OCP\Security\IRemoteHostValidator;
  16. use PHPUnit\Framework\MockObject\MockObject;
  17. use Psr\Log\LoggerInterface;
  18. use function parse_url;
  19. /**
  20. * Class ClientTest
  21. */
  22. class ClientTest extends \Test\TestCase {
  23. /** @var \GuzzleHttp\Client|MockObject */
  24. private $guzzleClient;
  25. /** @var CertificateManager|MockObject */
  26. private $certificateManager;
  27. /** @var Client */
  28. private $client;
  29. /** @var IConfig|MockObject */
  30. private $config;
  31. /** @var IRemoteHostValidator|MockObject */
  32. private IRemoteHostValidator $remoteHostValidator;
  33. private LoggerInterface $logger;
  34. /** @var array */
  35. private $defaultRequestOptions;
  36. protected function setUp(): void {
  37. parent::setUp();
  38. $this->config = $this->createMock(IConfig::class);
  39. $this->guzzleClient = $this->createMock(\GuzzleHttp\Client::class);
  40. $this->certificateManager = $this->createMock(ICertificateManager::class);
  41. $this->remoteHostValidator = $this->createMock(IRemoteHostValidator::class);
  42. $this->logger = $this->createMock(LoggerInterface::class);
  43. $this->client = new Client(
  44. $this->config,
  45. $this->certificateManager,
  46. $this->guzzleClient,
  47. $this->remoteHostValidator,
  48. $this->logger,
  49. );
  50. }
  51. public function testGetProxyUri(): void {
  52. $this->config
  53. ->method('getSystemValueString')
  54. ->with('proxy', '')
  55. ->willReturn('');
  56. $this->assertNull(self::invokePrivate($this->client, 'getProxyUri'));
  57. }
  58. public function testGetProxyUriProxyHostEmptyPassword(): void {
  59. $this->config
  60. ->method('getSystemValue')
  61. ->will($this->returnValueMap([
  62. ['proxyexclude', [], []],
  63. ]));
  64. $this->config
  65. ->method('getSystemValueString')
  66. ->will($this->returnValueMap([
  67. ['proxy', '', 'foo'],
  68. ['proxyuserpwd', '', ''],
  69. ]));
  70. $this->assertEquals([
  71. 'http' => 'foo',
  72. 'https' => 'foo'
  73. ], self::invokePrivate($this->client, 'getProxyUri'));
  74. }
  75. public function testGetProxyUriProxyHostWithPassword(): void {
  76. $this->config
  77. ->expects($this->once())
  78. ->method('getSystemValue')
  79. ->with('proxyexclude', [])
  80. ->willReturn([]);
  81. $this->config
  82. ->expects($this->exactly(2))
  83. ->method('getSystemValueString')
  84. ->withConsecutive(
  85. ['proxy', ''],
  86. ['proxyuserpwd', ''],
  87. )
  88. ->willReturnOnConsecutiveCalls(
  89. 'foo',
  90. 'username:password',
  91. );
  92. $this->assertEquals([
  93. 'http' => 'username:password@foo',
  94. 'https' => 'username:password@foo'
  95. ], self::invokePrivate($this->client, 'getProxyUri'));
  96. }
  97. public function testGetProxyUriProxyHostWithPasswordAndExclude(): void {
  98. $this->config
  99. ->expects($this->once())
  100. ->method('getSystemValue')
  101. ->with('proxyexclude', [])
  102. ->willReturn(['bar']);
  103. $this->config
  104. ->expects($this->exactly(2))
  105. ->method('getSystemValueString')
  106. ->withConsecutive(
  107. ['proxy', ''],
  108. ['proxyuserpwd', ''],
  109. )
  110. ->willReturnOnConsecutiveCalls(
  111. 'foo',
  112. 'username:password',
  113. );
  114. $this->assertEquals([
  115. 'http' => 'username:password@foo',
  116. 'https' => 'username:password@foo',
  117. 'no' => ['bar']
  118. ], self::invokePrivate($this->client, 'getProxyUri'));
  119. }
  120. public function dataPreventLocalAddress():array {
  121. return [
  122. ['https://localhost/foo.bar'],
  123. ['https://localHost/foo.bar'],
  124. ['https://random-host/foo.bar'],
  125. ['https://[::1]/bla.blub'],
  126. ['https://[::]/bla.blub'],
  127. ['https://192.168.0.1'],
  128. ['https://172.16.42.1'],
  129. ['https://[fdf8:f53b:82e4::53]/secret.ics'],
  130. ['https://[fe80::200:5aee:feaa:20a2]/secret.ics'],
  131. ['https://[0:0:0:0:0:0:10.0.0.1]/secret.ics'],
  132. ['https://[0:0:0:0:0:ffff:127.0.0.0]/secret.ics'],
  133. ['https://10.0.0.1'],
  134. ['https://another-host.local'],
  135. ['https://service.localhost'],
  136. ['!@#$', true], // test invalid url
  137. ['https://normal.host.com'],
  138. ['https://com.one-.nextcloud-one.com'],
  139. ];
  140. }
  141. /**
  142. * @dataProvider dataPreventLocalAddress
  143. * @param string $uri
  144. */
  145. public function testPreventLocalAddressDisabledByGlobalConfig(string $uri): void {
  146. $this->config->expects($this->once())
  147. ->method('getSystemValueBool')
  148. ->with('allow_local_remote_servers', false)
  149. ->willReturn(true);
  150. self::invokePrivate($this->client, 'preventLocalAddress', [$uri, []]);
  151. }
  152. /**
  153. * @dataProvider dataPreventLocalAddress
  154. * @param string $uri
  155. */
  156. public function testPreventLocalAddressDisabledByOption(string $uri): void {
  157. $this->config->expects($this->never())
  158. ->method('getSystemValueBool');
  159. self::invokePrivate($this->client, 'preventLocalAddress', [$uri, [
  160. 'nextcloud' => ['allow_local_address' => true],
  161. ]]);
  162. }
  163. /**
  164. * @dataProvider dataPreventLocalAddress
  165. * @param string $uri
  166. */
  167. public function testPreventLocalAddressOnGet(string $uri): void {
  168. $host = parse_url($uri, PHP_URL_HOST);
  169. $this->expectException(LocalServerException::class);
  170. $this->remoteHostValidator
  171. ->method('isValid')
  172. ->with($host)
  173. ->willReturn(false);
  174. $this->client->get($uri);
  175. }
  176. /**
  177. * @dataProvider dataPreventLocalAddress
  178. * @param string $uri
  179. */
  180. public function testPreventLocalAddressOnHead(string $uri): void {
  181. $host = parse_url($uri, PHP_URL_HOST);
  182. $this->expectException(LocalServerException::class);
  183. $this->remoteHostValidator
  184. ->method('isValid')
  185. ->with($host)
  186. ->willReturn(false);
  187. $this->client->head($uri);
  188. }
  189. /**
  190. * @dataProvider dataPreventLocalAddress
  191. * @param string $uri
  192. */
  193. public function testPreventLocalAddressOnPost(string $uri): void {
  194. $host = parse_url($uri, PHP_URL_HOST);
  195. $this->expectException(LocalServerException::class);
  196. $this->remoteHostValidator
  197. ->method('isValid')
  198. ->with($host)
  199. ->willReturn(false);
  200. $this->client->post($uri);
  201. }
  202. /**
  203. * @dataProvider dataPreventLocalAddress
  204. * @param string $uri
  205. */
  206. public function testPreventLocalAddressOnPut(string $uri): void {
  207. $host = parse_url($uri, PHP_URL_HOST);
  208. $this->expectException(LocalServerException::class);
  209. $this->remoteHostValidator
  210. ->method('isValid')
  211. ->with($host)
  212. ->willReturn(false);
  213. $this->client->put($uri);
  214. }
  215. /**
  216. * @dataProvider dataPreventLocalAddress
  217. * @param string $uri
  218. */
  219. public function testPreventLocalAddressOnDelete(string $uri): void {
  220. $host = parse_url($uri, PHP_URL_HOST);
  221. $this->expectException(LocalServerException::class);
  222. $this->remoteHostValidator
  223. ->method('isValid')
  224. ->with($host)
  225. ->willReturn(false);
  226. $this->client->delete($uri);
  227. }
  228. private function setUpDefaultRequestOptions(): void {
  229. $this->config
  230. ->method('getSystemValue')
  231. ->will($this->returnValueMap([
  232. ['proxyexclude', [], []],
  233. ]));
  234. $this->config
  235. ->method('getSystemValueString')
  236. ->will($this->returnValueMap([
  237. ['proxy', '', 'foo'],
  238. ['proxyuserpwd', '', ''],
  239. ]));
  240. $this->config
  241. ->method('getSystemValueBool')
  242. ->will($this->returnValueMap([
  243. ['installed', false, true],
  244. ['allow_local_remote_servers', false, true],
  245. ]));
  246. $this->certificateManager
  247. ->expects($this->once())
  248. ->method('getAbsoluteBundlePath')
  249. ->with()
  250. ->willReturn('/my/path.crt');
  251. $this->defaultRequestOptions = [
  252. 'verify' => '/my/path.crt',
  253. 'proxy' => [
  254. 'http' => 'foo',
  255. 'https' => 'foo'
  256. ],
  257. 'headers' => [
  258. 'User-Agent' => 'Nextcloud Server Crawler',
  259. 'Accept-Encoding' => 'gzip',
  260. ],
  261. 'timeout' => 30,
  262. 'nextcloud' => [
  263. 'allow_local_address' => true,
  264. ],
  265. ];
  266. }
  267. public function testGet(): void {
  268. $this->setUpDefaultRequestOptions();
  269. $this->guzzleClient->method('request')
  270. ->with('get', 'http://localhost/', $this->defaultRequestOptions)
  271. ->willReturn(new Response(418));
  272. $this->assertEquals(418, $this->client->get('http://localhost/', [])->getStatusCode());
  273. }
  274. public function testGetWithOptions(): void {
  275. $this->setUpDefaultRequestOptions();
  276. $options = array_merge($this->defaultRequestOptions, [
  277. 'verify' => false,
  278. 'proxy' => [
  279. 'http' => 'bar',
  280. 'https' => 'bar'
  281. ],
  282. ]);
  283. $this->guzzleClient->method('request')
  284. ->with('get', 'http://localhost/', $options)
  285. ->willReturn(new Response(418));
  286. $this->assertEquals(418, $this->client->get('http://localhost/', $options)->getStatusCode());
  287. }
  288. public function testPost(): void {
  289. $this->setUpDefaultRequestOptions();
  290. $this->guzzleClient->method('request')
  291. ->with('post', 'http://localhost/', $this->defaultRequestOptions)
  292. ->willReturn(new Response(418));
  293. $this->assertEquals(418, $this->client->post('http://localhost/', [])->getStatusCode());
  294. }
  295. public function testPostWithOptions(): void {
  296. $this->setUpDefaultRequestOptions();
  297. $options = array_merge($this->defaultRequestOptions, [
  298. 'verify' => false,
  299. 'proxy' => [
  300. 'http' => 'bar',
  301. 'https' => 'bar'
  302. ],
  303. ]);
  304. $this->guzzleClient->method('request')
  305. ->with('post', 'http://localhost/', $options)
  306. ->willReturn(new Response(418));
  307. $this->assertEquals(418, $this->client->post('http://localhost/', $options)->getStatusCode());
  308. }
  309. public function testPut(): void {
  310. $this->setUpDefaultRequestOptions();
  311. $this->guzzleClient->method('request')
  312. ->with('put', 'http://localhost/', $this->defaultRequestOptions)
  313. ->willReturn(new Response(418));
  314. $this->assertEquals(418, $this->client->put('http://localhost/', [])->getStatusCode());
  315. }
  316. public function testPutWithOptions(): void {
  317. $this->setUpDefaultRequestOptions();
  318. $options = array_merge($this->defaultRequestOptions, [
  319. 'verify' => false,
  320. 'proxy' => [
  321. 'http' => 'bar',
  322. 'https' => 'bar'
  323. ],
  324. ]);
  325. $this->guzzleClient->method('request')
  326. ->with('put', 'http://localhost/', $options)
  327. ->willReturn(new Response(418));
  328. $this->assertEquals(418, $this->client->put('http://localhost/', $options)->getStatusCode());
  329. }
  330. public function testDelete(): void {
  331. $this->setUpDefaultRequestOptions();
  332. $this->guzzleClient->method('request')
  333. ->with('delete', 'http://localhost/', $this->defaultRequestOptions)
  334. ->willReturn(new Response(418));
  335. $this->assertEquals(418, $this->client->delete('http://localhost/', [])->getStatusCode());
  336. }
  337. public function testDeleteWithOptions(): void {
  338. $this->setUpDefaultRequestOptions();
  339. $options = array_merge($this->defaultRequestOptions, [
  340. 'verify' => false,
  341. 'proxy' => [
  342. 'http' => 'bar',
  343. 'https' => 'bar'
  344. ],
  345. ]);
  346. $this->guzzleClient->method('request')
  347. ->with('delete', 'http://localhost/', $options)
  348. ->willReturn(new Response(418));
  349. $this->assertEquals(418, $this->client->delete('http://localhost/', $options)->getStatusCode());
  350. }
  351. public function testOptions(): void {
  352. $this->setUpDefaultRequestOptions();
  353. $this->guzzleClient->method('request')
  354. ->with('options', 'http://localhost/', $this->defaultRequestOptions)
  355. ->willReturn(new Response(418));
  356. $this->assertEquals(418, $this->client->options('http://localhost/', [])->getStatusCode());
  357. }
  358. public function testOptionsWithOptions(): void {
  359. $this->setUpDefaultRequestOptions();
  360. $options = array_merge($this->defaultRequestOptions, [
  361. 'verify' => false,
  362. 'proxy' => [
  363. 'http' => 'bar',
  364. 'https' => 'bar'
  365. ],
  366. ]);
  367. $this->guzzleClient->method('request')
  368. ->with('options', 'http://localhost/', $options)
  369. ->willReturn(new Response(418));
  370. $this->assertEquals(418, $this->client->options('http://localhost/', $options)->getStatusCode());
  371. }
  372. public function testHead(): void {
  373. $this->setUpDefaultRequestOptions();
  374. $this->guzzleClient->method('request')
  375. ->with('head', 'http://localhost/', $this->defaultRequestOptions)
  376. ->willReturn(new Response(418));
  377. $this->assertEquals(418, $this->client->head('http://localhost/', [])->getStatusCode());
  378. }
  379. public function testHeadWithOptions(): void {
  380. $this->setUpDefaultRequestOptions();
  381. $options = array_merge($this->defaultRequestOptions, [
  382. 'verify' => false,
  383. 'proxy' => [
  384. 'http' => 'bar',
  385. 'https' => 'bar'
  386. ],
  387. ]);
  388. $this->guzzleClient->method('request')
  389. ->with('head', 'http://localhost/', $options)
  390. ->willReturn(new Response(418));
  391. $this->assertEquals(418, $this->client->head('http://localhost/', $options)->getStatusCode());
  392. }
  393. public function testSetDefaultOptionsWithNotInstalled(): void {
  394. $this->config
  395. ->expects($this->exactly(2))
  396. ->method('getSystemValueBool')
  397. ->withConsecutive(
  398. ['installed', false],
  399. ['allow_local_remote_servers', false],
  400. )
  401. ->willReturnOnConsecutiveCalls(
  402. false,
  403. false,
  404. );
  405. $this->config
  406. ->expects($this->once())
  407. ->method('getSystemValueString')
  408. ->with('proxy', '')
  409. ->willReturn('');
  410. $this->certificateManager
  411. ->expects($this->never())
  412. ->method('listCertificates');
  413. $this->assertEquals([
  414. 'verify' => \OC::$SERVERROOT . '/resources/config/ca-bundle.crt',
  415. 'headers' => [
  416. 'User-Agent' => 'Nextcloud Server Crawler',
  417. 'Accept-Encoding' => 'gzip',
  418. ],
  419. 'timeout' => 30,
  420. 'nextcloud' => [
  421. 'allow_local_address' => false,
  422. ],
  423. 'allow_redirects' => [
  424. 'on_redirect' => function (
  425. \Psr\Http\Message\RequestInterface $request,
  426. \Psr\Http\Message\ResponseInterface $response,
  427. \Psr\Http\Message\UriInterface $uri
  428. ) {
  429. },
  430. ],
  431. ], self::invokePrivate($this->client, 'buildRequestOptions', [[]]));
  432. }
  433. public function testSetDefaultOptionsWithProxy(): void {
  434. $this->config
  435. ->expects($this->exactly(2))
  436. ->method('getSystemValueBool')
  437. ->withConsecutive(
  438. ['installed', false],
  439. ['allow_local_remote_servers', false],
  440. )
  441. ->willReturnOnConsecutiveCalls(
  442. true,
  443. false,
  444. );
  445. $this->config
  446. ->expects($this->once())
  447. ->method('getSystemValue')
  448. ->with('proxyexclude', [])
  449. ->willReturn([]);
  450. $this->config
  451. ->expects($this->exactly(2))
  452. ->method('getSystemValueString')
  453. ->withConsecutive(
  454. ['proxy', ''],
  455. ['proxyuserpwd', ''],
  456. )
  457. ->willReturnOnConsecutiveCalls(
  458. 'foo',
  459. '',
  460. );
  461. $this->certificateManager
  462. ->expects($this->once())
  463. ->method('getAbsoluteBundlePath')
  464. ->with()
  465. ->willReturn('/my/path.crt');
  466. $this->assertEquals([
  467. 'verify' => '/my/path.crt',
  468. 'proxy' => [
  469. 'http' => 'foo',
  470. 'https' => 'foo'
  471. ],
  472. 'headers' => [
  473. 'User-Agent' => 'Nextcloud Server Crawler',
  474. 'Accept-Encoding' => 'gzip',
  475. ],
  476. 'timeout' => 30,
  477. 'nextcloud' => [
  478. 'allow_local_address' => false,
  479. ],
  480. 'allow_redirects' => [
  481. 'on_redirect' => function (
  482. \Psr\Http\Message\RequestInterface $request,
  483. \Psr\Http\Message\ResponseInterface $response,
  484. \Psr\Http\Message\UriInterface $uri
  485. ) {
  486. },
  487. ],
  488. ], self::invokePrivate($this->client, 'buildRequestOptions', [[]]));
  489. }
  490. public function testSetDefaultOptionsWithProxyAndExclude(): void {
  491. $this->config
  492. ->expects($this->exactly(2))
  493. ->method('getSystemValueBool')
  494. ->withConsecutive(
  495. ['installed', false],
  496. ['allow_local_remote_servers', false],
  497. )
  498. ->willReturnOnConsecutiveCalls(
  499. true,
  500. false,
  501. );
  502. $this->config
  503. ->expects($this->once())
  504. ->method('getSystemValue')
  505. ->with('proxyexclude', [])
  506. ->willReturn(['bar']);
  507. $this->config
  508. ->expects($this->exactly(2))
  509. ->method('getSystemValueString')
  510. ->withConsecutive(
  511. ['proxy', ''],
  512. ['proxyuserpwd', ''],
  513. )
  514. ->willReturnOnConsecutiveCalls(
  515. 'foo',
  516. '',
  517. );
  518. $this->certificateManager
  519. ->expects($this->once())
  520. ->method('getAbsoluteBundlePath')
  521. ->with()
  522. ->willReturn('/my/path.crt');
  523. $this->assertEquals([
  524. 'verify' => '/my/path.crt',
  525. 'proxy' => [
  526. 'http' => 'foo',
  527. 'https' => 'foo',
  528. 'no' => ['bar']
  529. ],
  530. 'headers' => [
  531. 'User-Agent' => 'Nextcloud Server Crawler',
  532. 'Accept-Encoding' => 'gzip',
  533. ],
  534. 'timeout' => 30,
  535. 'nextcloud' => [
  536. 'allow_local_address' => false,
  537. ],
  538. 'allow_redirects' => [
  539. 'on_redirect' => function (
  540. \Psr\Http\Message\RequestInterface $request,
  541. \Psr\Http\Message\ResponseInterface $response,
  542. \Psr\Http\Message\UriInterface $uri
  543. ) {
  544. },
  545. ],
  546. ], self::invokePrivate($this->client, 'buildRequestOptions', [[]]));
  547. }
  548. }