ClientTest.php 15 KB

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