DnsPinMiddlewareTest.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace lib\Http\Client;
  8. use GuzzleHttp\Handler\MockHandler;
  9. use GuzzleHttp\HandlerStack;
  10. use GuzzleHttp\Psr7\Request;
  11. use GuzzleHttp\Psr7\Response;
  12. use OC\Http\Client\DnsPinMiddleware;
  13. use OC\Http\Client\NegativeDnsCache;
  14. use OC\Memcache\NullCache;
  15. use OC\Net\IpAddressClassifier;
  16. use OCP\Http\Client\LocalServerException;
  17. use OCP\ICacheFactory;
  18. use Psr\Http\Message\RequestInterface;
  19. use Test\TestCase;
  20. class DnsPinMiddlewareTest extends TestCase {
  21. private DnsPinMiddleware $dnsPinMiddleware;
  22. protected function setUp(): void {
  23. parent::setUp();
  24. $cacheFactory = $this->createMock(ICacheFactory::class);
  25. $cacheFactory
  26. ->method('createLocal')
  27. ->willReturn(new NullCache());
  28. $ipAddressClassifier = new IpAddressClassifier();
  29. $negativeDnsCache = new NegativeDnsCache($cacheFactory);
  30. $this->dnsPinMiddleware = $this->getMockBuilder(DnsPinMiddleware::class)
  31. ->setConstructorArgs([$negativeDnsCache, $ipAddressClassifier])
  32. ->onlyMethods(['dnsGetRecord'])
  33. ->getMock();
  34. }
  35. public function testPopulateDnsCacheIPv4(): void {
  36. $mockHandler = new MockHandler([
  37. static function (RequestInterface $request, array $options) {
  38. self::arrayHasKey('curl', $options);
  39. self::arrayHasKey(CURLOPT_RESOLVE, $options['curl']);
  40. self::assertEquals([
  41. 'www.example.com:80:1.1.1.1',
  42. 'www.example.com:443:1.1.1.1'
  43. ], $options['curl'][CURLOPT_RESOLVE]);
  44. return new Response(200);
  45. },
  46. ]);
  47. $this->dnsPinMiddleware
  48. ->method('dnsGetRecord')
  49. ->willReturnCallback(function (string $hostname, int $type) {
  50. // example.com SOA
  51. if ($hostname === 'example.com') {
  52. return match ($type) {
  53. DNS_SOA => [
  54. [
  55. 'host' => 'example.com',
  56. 'class' => 'IN',
  57. 'ttl' => 7079,
  58. 'type' => 'SOA',
  59. 'minimum-ttl' => 3600,
  60. ]
  61. ],
  62. };
  63. }
  64. // example.com A, AAAA, CNAME
  65. if ($hostname === 'www.example.com') {
  66. return match ($type) {
  67. DNS_A => [],
  68. DNS_AAAA => [],
  69. DNS_CNAME => [
  70. [
  71. 'host' => 'www.example.com',
  72. 'class' => 'IN',
  73. 'ttl' => 1800,
  74. 'type' => 'A',
  75. 'target' => 'www.example.net'
  76. ]
  77. ],
  78. };
  79. }
  80. // example.net SOA
  81. if ($hostname === 'example.net') {
  82. return match ($type) {
  83. DNS_SOA => [
  84. [
  85. 'host' => 'example.net',
  86. 'class' => 'IN',
  87. 'ttl' => 7079,
  88. 'type' => 'SOA',
  89. 'minimum-ttl' => 3600,
  90. ]
  91. ],
  92. };
  93. }
  94. // example.net A, AAAA, CNAME
  95. if ($hostname === 'www.example.net') {
  96. return match ($type) {
  97. DNS_A => [
  98. [
  99. 'host' => 'www.example.net',
  100. 'class' => 'IN',
  101. 'ttl' => 1800,
  102. 'type' => 'A',
  103. 'ip' => '1.1.1.1'
  104. ]
  105. ],
  106. DNS_AAAA => [],
  107. DNS_CNAME => [],
  108. };
  109. }
  110. return false;
  111. });
  112. $stack = new HandlerStack($mockHandler);
  113. $stack->push($this->dnsPinMiddleware->addDnsPinning());
  114. $handler = $stack->resolve();
  115. $handler(
  116. new Request('GET', 'https://www.example.com'),
  117. ['nextcloud' => ['allow_local_address' => false]]
  118. );
  119. }
  120. public function testPopulateDnsCacheIPv6(): void {
  121. $mockHandler = new MockHandler([
  122. static function (RequestInterface $request, array $options) {
  123. self::arrayHasKey('curl', $options);
  124. self::arrayHasKey(CURLOPT_RESOLVE, $options['curl']);
  125. self::assertEquals([
  126. 'www.example.com:80:1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001',
  127. 'www.example.com:443:1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001'
  128. ], $options['curl'][CURLOPT_RESOLVE]);
  129. return new Response(200);
  130. },
  131. ]);
  132. $this->dnsPinMiddleware
  133. ->method('dnsGetRecord')
  134. ->willReturnCallback(function (string $hostname, int $type) {
  135. // example.com SOA
  136. if ($hostname === 'example.com') {
  137. return match ($type) {
  138. DNS_SOA => [
  139. [
  140. 'host' => 'example.com',
  141. 'class' => 'IN',
  142. 'ttl' => 7079,
  143. 'type' => 'SOA',
  144. 'minimum-ttl' => 3600,
  145. ]
  146. ],
  147. };
  148. }
  149. // example.com A, AAAA, CNAME
  150. if ($hostname === 'www.example.com') {
  151. return match ($type) {
  152. DNS_A => [],
  153. DNS_AAAA => [],
  154. DNS_CNAME => [
  155. [
  156. 'host' => 'www.example.com',
  157. 'class' => 'IN',
  158. 'ttl' => 1800,
  159. 'type' => 'A',
  160. 'target' => 'www.example.net'
  161. ]
  162. ],
  163. };
  164. }
  165. // example.net SOA
  166. if ($hostname === 'example.net') {
  167. return match ($type) {
  168. DNS_SOA => [
  169. [
  170. 'host' => 'example.net',
  171. 'class' => 'IN',
  172. 'ttl' => 7079,
  173. 'type' => 'SOA',
  174. 'minimum-ttl' => 3600,
  175. ]
  176. ],
  177. };
  178. }
  179. // example.net A, AAAA, CNAME
  180. if ($hostname === 'www.example.net') {
  181. return match ($type) {
  182. DNS_A => [
  183. [
  184. 'host' => 'www.example.net',
  185. 'class' => 'IN',
  186. 'ttl' => 1800,
  187. 'type' => 'A',
  188. 'ip' => '1.1.1.1'
  189. ],
  190. [
  191. 'host' => 'www.example.net',
  192. 'class' => 'IN',
  193. 'ttl' => 1800,
  194. 'type' => 'A',
  195. 'ip' => '1.0.0.1'
  196. ],
  197. ],
  198. DNS_AAAA => [
  199. [
  200. 'host' => 'www.example.net',
  201. 'class' => 'IN',
  202. 'ttl' => 1800,
  203. 'type' => 'AAAA',
  204. 'ip' => '2606:4700:4700::1111'
  205. ],
  206. [
  207. 'host' => 'www.example.net',
  208. 'class' => 'IN',
  209. 'ttl' => 1800,
  210. 'type' => 'AAAA',
  211. 'ip' => '2606:4700:4700::1001'
  212. ],
  213. ],
  214. DNS_CNAME => [],
  215. };
  216. }
  217. return false;
  218. });
  219. $stack = new HandlerStack($mockHandler);
  220. $stack->push($this->dnsPinMiddleware->addDnsPinning());
  221. $handler = $stack->resolve();
  222. $handler(
  223. new Request('GET', 'https://www.example.com'),
  224. ['nextcloud' => ['allow_local_address' => false]]
  225. );
  226. }
  227. public function testAllowLocalAddress(): void {
  228. $mockHandler = new MockHandler([
  229. static function (RequestInterface $request, array $options) {
  230. self::assertArrayNotHasKey('curl', $options);
  231. return new Response(200);
  232. },
  233. ]);
  234. $stack = new HandlerStack($mockHandler);
  235. $stack->push($this->dnsPinMiddleware->addDnsPinning());
  236. $handler = $stack->resolve();
  237. $handler(
  238. new Request('GET', 'https://www.example.com'),
  239. ['nextcloud' => ['allow_local_address' => true]]
  240. );
  241. }
  242. public function testRejectIPv4(): void {
  243. $this->expectException(LocalServerException::class);
  244. $this->expectExceptionMessage('violates local access rules');
  245. $mockHandler = new MockHandler([
  246. static function (RequestInterface $request, array $options) {
  247. // The handler should not be called
  248. },
  249. ]);
  250. $this->dnsPinMiddleware
  251. ->method('dnsGetRecord')
  252. ->willReturnCallback(function (string $hostname, int $type) {
  253. return match ($type) {
  254. DNS_SOA => [
  255. [
  256. 'host' => 'example.com',
  257. 'class' => 'IN',
  258. 'ttl' => 7079,
  259. 'type' => 'SOA',
  260. 'minimum-ttl' => 3600,
  261. ]
  262. ],
  263. DNS_A => [
  264. [
  265. 'host' => 'example.com',
  266. 'class' => 'IN',
  267. 'ttl' => 1800,
  268. 'type' => 'A',
  269. 'ip' => '192.168.0.1'
  270. ]
  271. ],
  272. DNS_AAAA => [],
  273. DNS_CNAME => [],
  274. };
  275. });
  276. $stack = new HandlerStack($mockHandler);
  277. $stack->push($this->dnsPinMiddleware->addDnsPinning());
  278. $handler = $stack->resolve();
  279. $handler(
  280. new Request('GET', 'https://www.example.com'),
  281. ['nextcloud' => ['allow_local_address' => false]]
  282. );
  283. }
  284. public function testRejectIPv6(): void {
  285. $this->expectException(LocalServerException::class);
  286. $this->expectExceptionMessage('violates local access rules');
  287. $mockHandler = new MockHandler([
  288. static function (RequestInterface $request, array $options) {
  289. // The handler should not be called
  290. },
  291. ]);
  292. $this->dnsPinMiddleware
  293. ->method('dnsGetRecord')
  294. ->willReturnCallback(function (string $hostname, int $type) {
  295. return match ($type) {
  296. DNS_SOA => [
  297. [
  298. 'host' => 'example.com',
  299. 'class' => 'IN',
  300. 'ttl' => 7079,
  301. 'type' => 'SOA',
  302. 'minimum-ttl' => 3600,
  303. ]
  304. ],
  305. DNS_A => [],
  306. DNS_AAAA => [
  307. [
  308. 'host' => 'ipv6.example.com',
  309. 'class' => 'IN',
  310. 'ttl' => 1800,
  311. 'type' => 'AAAA',
  312. 'ipv6' => 'fd12:3456:789a:1::1'
  313. ]
  314. ],
  315. DNS_CNAME => [],
  316. };
  317. });
  318. $stack = new HandlerStack($mockHandler);
  319. $stack->push($this->dnsPinMiddleware->addDnsPinning());
  320. $handler = $stack->resolve();
  321. $handler(
  322. new Request('GET', 'https://ipv6.example.com'),
  323. ['nextcloud' => ['allow_local_address' => false]]
  324. );
  325. }
  326. public function testRejectCanonicalName(): void {
  327. $this->expectException(LocalServerException::class);
  328. $this->expectExceptionMessage('violates local access rules');
  329. $mockHandler = new MockHandler([
  330. static function (RequestInterface $request, array $options) {
  331. // The handler should not be called
  332. },
  333. ]);
  334. $this->dnsPinMiddleware
  335. ->method('dnsGetRecord')
  336. ->willReturnCallback(function (string $hostname, int $type) {
  337. // example.com SOA
  338. if ($hostname === 'example.com') {
  339. return match ($type) {
  340. DNS_SOA => [
  341. [
  342. 'host' => 'example.com',
  343. 'class' => 'IN',
  344. 'ttl' => 7079,
  345. 'type' => 'SOA',
  346. 'minimum-ttl' => 3600,
  347. ]
  348. ],
  349. };
  350. }
  351. // example.com A, AAAA, CNAME
  352. if ($hostname === 'www.example.com') {
  353. return match ($type) {
  354. DNS_A => [],
  355. DNS_AAAA => [],
  356. DNS_CNAME => [
  357. [
  358. 'host' => 'www.example.com',
  359. 'class' => 'IN',
  360. 'ttl' => 1800,
  361. 'type' => 'A',
  362. 'target' => 'www.example.net'
  363. ]
  364. ],
  365. };
  366. }
  367. // example.net SOA
  368. if ($hostname === 'example.net') {
  369. return match ($type) {
  370. DNS_SOA => [
  371. [
  372. 'host' => 'example.net',
  373. 'class' => 'IN',
  374. 'ttl' => 7079,
  375. 'type' => 'SOA',
  376. 'minimum-ttl' => 3600,
  377. ]
  378. ],
  379. };
  380. }
  381. // example.net A, AAAA, CNAME
  382. if ($hostname === 'www.example.net') {
  383. return match ($type) {
  384. DNS_A => [
  385. [
  386. 'host' => 'www.example.net',
  387. 'class' => 'IN',
  388. 'ttl' => 1800,
  389. 'type' => 'A',
  390. 'ip' => '192.168.0.2'
  391. ]
  392. ],
  393. DNS_AAAA => [],
  394. DNS_CNAME => [],
  395. };
  396. }
  397. return false;
  398. });
  399. $stack = new HandlerStack($mockHandler);
  400. $stack->push($this->dnsPinMiddleware->addDnsPinning());
  401. $handler = $stack->resolve();
  402. $handler(
  403. new Request('GET', 'https://www.example.com'),
  404. ['nextcloud' => ['allow_local_address' => false]]
  405. );
  406. }
  407. public function testRejectFaultyResponse(): void {
  408. $this->expectException(LocalServerException::class);
  409. $this->expectExceptionMessage('No DNS record found for www.example.com');
  410. $mockHandler = new MockHandler([
  411. static function (RequestInterface $request, array $options) {
  412. // The handler should not be called
  413. },
  414. ]);
  415. $this->dnsPinMiddleware
  416. ->method('dnsGetRecord')
  417. ->willReturnCallback(function (string $hostname, int $type) {
  418. return false;
  419. });
  420. $stack = new HandlerStack($mockHandler);
  421. $stack->push($this->dnsPinMiddleware->addDnsPinning());
  422. $handler = $stack->resolve();
  423. $handler(
  424. new Request('GET', 'https://www.example.com'),
  425. ['nextcloud' => ['allow_local_address' => false]]
  426. );
  427. }
  428. public function testIgnoreSubdomainForSoaQuery(): void {
  429. $mockHandler = new MockHandler([
  430. static function (RequestInterface $request, array $options) {
  431. // The handler should not be called
  432. },
  433. ]);
  434. $dnsQueries = [];
  435. $this->dnsPinMiddleware
  436. ->method('dnsGetRecord')
  437. ->willReturnCallback(function (string $hostname, int $type) use (&$dnsQueries) {
  438. // log query
  439. $dnsQueries[] = $hostname . $type;
  440. // example.com SOA
  441. if ($hostname === 'example.com') {
  442. return match ($type) {
  443. DNS_SOA => [
  444. [
  445. 'host' => 'example.com',
  446. 'class' => 'IN',
  447. 'ttl' => 7079,
  448. 'type' => 'SOA',
  449. 'minimum-ttl' => 3600,
  450. ]
  451. ],
  452. };
  453. }
  454. // example.net A, AAAA, CNAME
  455. if ($hostname === 'subsubdomain.subdomain.example.com') {
  456. return match ($type) {
  457. DNS_A => [
  458. [
  459. 'host' => 'subsubdomain.subdomain.example.com',
  460. 'class' => 'IN',
  461. 'ttl' => 1800,
  462. 'type' => 'A',
  463. 'ip' => '1.1.1.1'
  464. ]
  465. ],
  466. DNS_AAAA => [],
  467. DNS_CNAME => [],
  468. };
  469. }
  470. return false;
  471. });
  472. $stack = new HandlerStack($mockHandler);
  473. $stack->push($this->dnsPinMiddleware->addDnsPinning());
  474. $handler = $stack->resolve();
  475. $handler(
  476. new Request('GET', 'https://subsubdomain.subdomain.example.com'),
  477. ['nextcloud' => ['allow_local_address' => false]]
  478. );
  479. $this->assertCount(4, $dnsQueries);
  480. $this->assertContains('example.com' . DNS_SOA, $dnsQueries);
  481. $this->assertContains('subsubdomain.subdomain.example.com' . DNS_A, $dnsQueries);
  482. $this->assertContains('subsubdomain.subdomain.example.com' . DNS_AAAA, $dnsQueries);
  483. $this->assertContains('subsubdomain.subdomain.example.com' . DNS_CNAME, $dnsQueries);
  484. }
  485. }