diff options
Diffstat (limited to 'tests/lib/Http/Client')
-rw-r--r-- | tests/lib/Http/Client/ClientServiceTest.php | 80 | ||||
-rw-r--r-- | tests/lib/Http/Client/ClientTest.php | 246 | ||||
-rw-r--r-- | tests/lib/Http/Client/DnsPinMiddlewareTest.php | 547 | ||||
-rw-r--r-- | tests/lib/Http/Client/NegativeDnsCacheTest.php | 29 | ||||
-rw-r--r-- | tests/lib/Http/Client/ResponseTest.php | 16 |
5 files changed, 753 insertions, 165 deletions
diff --git a/tests/lib/Http/Client/ClientServiceTest.php b/tests/lib/Http/Client/ClientServiceTest.php index ed1165236aa..fd5b155ca69 100644 --- a/tests/lib/Http/Client/ClientServiceTest.php +++ b/tests/lib/Http/Client/ClientServiceTest.php @@ -3,23 +3,26 @@ declare(strict_types=1); /** - * Copyright (c) 2015 Lukas Reschke <lukas@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Http\Client; use GuzzleHttp\Client as GuzzleClient; -use GuzzleHttp\HandlerStack; use GuzzleHttp\Handler\CurlHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; use OC\Http\Client\Client; use OC\Http\Client\ClientService; use OC\Http\Client\DnsPinMiddleware; +use OCP\Diagnostics\IEventLogger; use OCP\ICertificateManager; use OCP\IConfig; use OCP\Security\IRemoteHostValidator; +use Psr\Http\Message\RequestInterface; +use Psr\Log\LoggerInterface; /** * Class ClientServiceTest @@ -28,26 +31,86 @@ class ClientServiceTest extends \Test\TestCase { public function testNewClient(): void { /** @var IConfig $config */ $config = $this->createMock(IConfig::class); + $config->method('getSystemValueBool') + ->with('dns_pinning', true) + ->willReturn(true); /** @var ICertificateManager $certificateManager */ $certificateManager = $this->createMock(ICertificateManager::class); $dnsPinMiddleware = $this->createMock(DnsPinMiddleware::class); $dnsPinMiddleware ->expects($this->atLeastOnce()) ->method('addDnsPinning') - ->willReturn(function () { + ->willReturn(function (): void { }); $remoteHostValidator = $this->createMock(IRemoteHostValidator::class); + $eventLogger = $this->createMock(IEventLogger::class); + $logger = $this->createMock(LoggerInterface::class); $clientService = new ClientService( $config, $certificateManager, $dnsPinMiddleware, - $remoteHostValidator + $remoteHostValidator, + $eventLogger, + $logger, ); $handler = new CurlHandler(); $stack = HandlerStack::create($handler); $stack->push($dnsPinMiddleware->addDnsPinning()); + $stack->push(Middleware::tap(function (RequestInterface $request) use ($eventLogger): void { + $eventLogger->start('http:request', $request->getMethod() . ' request to ' . $request->getRequestTarget()); + }, function () use ($eventLogger): void { + $eventLogger->end('http:request'); + }), 'event logger'); + $guzzleClient = new GuzzleClient(['handler' => $stack]); + + $this->assertEquals( + new Client( + $config, + $certificateManager, + $guzzleClient, + $remoteHostValidator, + $logger, + ), + $clientService->newClient() + ); + } + + public function testDisableDnsPinning(): void { + /** @var IConfig $config */ + $config = $this->createMock(IConfig::class); + $config->method('getSystemValueBool') + ->with('dns_pinning', true) + ->willReturn(false); + /** @var ICertificateManager $certificateManager */ + $certificateManager = $this->createMock(ICertificateManager::class); + $dnsPinMiddleware = $this->createMock(DnsPinMiddleware::class); + $dnsPinMiddleware + ->expects($this->never()) + ->method('addDnsPinning') + ->willReturn(function (): void { + }); + $remoteHostValidator = $this->createMock(IRemoteHostValidator::class); + $eventLogger = $this->createMock(IEventLogger::class); + $logger = $this->createMock(LoggerInterface::class); + + $clientService = new ClientService( + $config, + $certificateManager, + $dnsPinMiddleware, + $remoteHostValidator, + $eventLogger, + $logger, + ); + + $handler = new CurlHandler(); + $stack = HandlerStack::create($handler); + $stack->push(Middleware::tap(function (RequestInterface $request) use ($eventLogger): void { + $eventLogger->start('http:request', $request->getMethod() . ' request to ' . $request->getRequestTarget()); + }, function () use ($eventLogger): void { + $eventLogger->end('http:request'); + }), 'event logger'); $guzzleClient = new GuzzleClient(['handler' => $stack]); $this->assertEquals( @@ -55,7 +118,8 @@ class ClientServiceTest extends \Test\TestCase { $config, $certificateManager, $guzzleClient, - $remoteHostValidator + $remoteHostValidator, + $logger, ), $clientService->newClient() ); diff --git a/tests/lib/Http/Client/ClientTest.php b/tests/lib/Http/Client/ClientTest.php index 93948a5daf3..e76b66b52d7 100644 --- a/tests/lib/Http/Client/ClientTest.php +++ b/tests/lib/Http/Client/ClientTest.php @@ -3,10 +3,9 @@ declare(strict_types=1); /** - * Copyright (c) 2015 Lukas Reschke <lukas@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Http\Client; @@ -19,6 +18,7 @@ use OCP\ICertificateManager; use OCP\IConfig; use OCP\Security\IRemoteHostValidator; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; use function parse_url; /** @@ -35,6 +35,7 @@ class ClientTest extends \Test\TestCase { private $config; /** @var IRemoteHostValidator|MockObject */ private IRemoteHostValidator $remoteHostValidator; + private LoggerInterface $logger; /** @var array */ private $defaultRequestOptions; @@ -44,32 +45,37 @@ class ClientTest extends \Test\TestCase { $this->guzzleClient = $this->createMock(\GuzzleHttp\Client::class); $this->certificateManager = $this->createMock(ICertificateManager::class); $this->remoteHostValidator = $this->createMock(IRemoteHostValidator::class); + $this->logger = $this->createMock(LoggerInterface::class); $this->client = new Client( $this->config, $this->certificateManager, $this->guzzleClient, - $this->remoteHostValidator + $this->remoteHostValidator, + $this->logger, ); } public function testGetProxyUri(): void { $this->config - ->method('getSystemValue') - ->with('proxy', null) - ->willReturn(null); + ->method('getSystemValueString') + ->with('proxy', '') + ->willReturn(''); $this->assertNull(self::invokePrivate($this->client, 'getProxyUri')); } public function testGetProxyUriProxyHostEmptyPassword(): void { - $map = [ - ['proxy', '', 'foo'], - ['proxyuserpwd', '', null], - ['proxyexclude', [], []], - ]; - $this->config ->method('getSystemValue') - ->will($this->returnValueMap($map)); + ->willReturnMap([ + ['proxyexclude', [], []], + ]); + + $this->config + ->method('getSystemValueString') + ->willReturnMap([ + ['proxy', '', 'foo'], + ['proxyuserpwd', '', ''], + ]); $this->assertEquals([ 'http' => 'foo', @@ -79,33 +85,17 @@ class ClientTest extends \Test\TestCase { public function testGetProxyUriProxyHostWithPassword(): void { $this->config - ->expects($this->exactly(3)) + ->expects($this->once()) ->method('getSystemValue') - ->withConsecutive( - [ - $this->equalTo('proxy'), - $this->callback(function ($input) { - return $input === ''; - }) - ], - [ - $this->equalTo('proxyuserpwd'), - $this->callback(function ($input) { - return $input === ''; - }) - ], - [ - $this->equalTo('proxyexclude'), - $this->callback(function ($input) { - return $input === []; - }) - ], - ) - ->willReturnOnConsecutiveCalls( - 'foo', - 'username:password', - [], - ); + ->with('proxyexclude', []) + ->willReturn([]); + $this->config + ->expects($this->exactly(2)) + ->method('getSystemValueString') + ->willReturnMap([ + ['proxy', '', 'foo'], + ['proxyuserpwd', '', 'username:password'], + ]); $this->assertEquals([ 'http' => 'username:password@foo', 'https' => 'username:password@foo' @@ -114,33 +104,17 @@ class ClientTest extends \Test\TestCase { public function testGetProxyUriProxyHostWithPasswordAndExclude(): void { $this->config - ->expects($this->exactly(3)) + ->expects($this->once()) ->method('getSystemValue') - ->withConsecutive( - [ - $this->equalTo('proxy'), - $this->callback(function ($input) { - return $input === ''; - }) - ], - [ - $this->equalTo('proxyuserpwd'), - $this->callback(function ($input) { - return $input === ''; - }) - ], - [ - $this->equalTo('proxyexclude'), - $this->callback(function ($input) { - return $input === []; - }) - ], - ) - ->willReturnOnConsecutiveCalls( - 'foo', - 'username:password', - ['bar'], - ); + ->with('proxyexclude', []) + ->willReturn(['bar']); + $this->config + ->expects($this->exactly(2)) + ->method('getSystemValueString') + ->willReturnMap([ + ['proxy', '', 'foo'], + ['proxyuserpwd', '', 'username:password'], + ]); $this->assertEquals([ 'http' => 'username:password@foo', 'https' => 'username:password@foo', @@ -148,7 +122,14 @@ class ClientTest extends \Test\TestCase { ], self::invokePrivate($this->client, 'getProxyUri')); } - public function dataPreventLocalAddress():array { + public function testPreventLocalAddressThrowOnInvalidUri(): void { + $this->expectException(LocalServerException::class); + $this->expectExceptionMessage('Could not detect any host'); + + self::invokePrivate($this->client, 'preventLocalAddress', ['!@#$', []]); + } + + public static function dataPreventLocalAddress(): array { return [ ['https://localhost/foo.bar'], ['https://localHost/foo.bar'], @@ -164,15 +145,15 @@ class ClientTest extends \Test\TestCase { ['https://10.0.0.1'], ['https://another-host.local'], ['https://service.localhost'], - ['!@#$', true], // test invalid url ['https://normal.host.com'], + ['https://com.one-.nextcloud-one.com'], ]; } /** - * @dataProvider dataPreventLocalAddress * @param string $uri */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataPreventLocalAddress')] public function testPreventLocalAddressDisabledByGlobalConfig(string $uri): void { $this->config->expects($this->once()) ->method('getSystemValueBool') @@ -183,9 +164,9 @@ class ClientTest extends \Test\TestCase { } /** - * @dataProvider dataPreventLocalAddress * @param string $uri */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataPreventLocalAddress')] public function testPreventLocalAddressDisabledByOption(string $uri): void { $this->config->expects($this->never()) ->method('getSystemValueBool'); @@ -196,9 +177,9 @@ class ClientTest extends \Test\TestCase { } /** - * @dataProvider dataPreventLocalAddress * @param string $uri */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataPreventLocalAddress')] public function testPreventLocalAddressOnGet(string $uri): void { $host = parse_url($uri, PHP_URL_HOST); $this->expectException(LocalServerException::class); @@ -211,9 +192,9 @@ class ClientTest extends \Test\TestCase { } /** - * @dataProvider dataPreventLocalAddress * @param string $uri */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataPreventLocalAddress')] public function testPreventLocalAddressOnHead(string $uri): void { $host = parse_url($uri, PHP_URL_HOST); $this->expectException(LocalServerException::class); @@ -226,9 +207,9 @@ class ClientTest extends \Test\TestCase { } /** - * @dataProvider dataPreventLocalAddress * @param string $uri */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataPreventLocalAddress')] public function testPreventLocalAddressOnPost(string $uri): void { $host = parse_url($uri, PHP_URL_HOST); $this->expectException(LocalServerException::class); @@ -241,9 +222,9 @@ class ClientTest extends \Test\TestCase { } /** - * @dataProvider dataPreventLocalAddress * @param string $uri */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataPreventLocalAddress')] public function testPreventLocalAddressOnPut(string $uri): void { $host = parse_url($uri, PHP_URL_HOST); $this->expectException(LocalServerException::class); @@ -256,9 +237,9 @@ class ClientTest extends \Test\TestCase { } /** - * @dataProvider dataPreventLocalAddress * @param string $uri */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataPreventLocalAddress')] public function testPreventLocalAddressOnDelete(string $uri): void { $host = parse_url($uri, PHP_URL_HOST); $this->expectException(LocalServerException::class); @@ -271,19 +252,23 @@ class ClientTest extends \Test\TestCase { } private function setUpDefaultRequestOptions(): void { - $map = [ - ['proxy', '', 'foo'], - ['proxyuserpwd', '', null], - ['proxyexclude', [], []], - ]; - $this->config ->method('getSystemValue') - ->will($this->returnValueMap($map)); + ->willReturnMap([ + ['proxyexclude', [], []], + ]); + $this->config + ->method('getSystemValueString') + ->willReturnMap([ + ['proxy', '', 'foo'], + ['proxyuserpwd', '', ''], + ]); $this->config ->method('getSystemValueBool') - ->with('allow_local_remote_servers', false) - ->willReturn(true); + ->willReturnMap([ + ['installed', false, true], + ['allow_local_remote_servers', false, true], + ]); $this->certificateManager ->expects($this->once()) @@ -467,15 +452,16 @@ class ClientTest extends \Test\TestCase { public function testSetDefaultOptionsWithNotInstalled(): void { $this->config ->expects($this->exactly(2)) - ->method('getSystemValue') - ->withConsecutive( - ['proxy', ''], - ['installed', false], - ) - ->willReturnOnConsecutiveCalls( - '', - false, - ); + ->method('getSystemValueBool') + ->willReturnMap([ + ['installed', false, false], + ['allow_local_remote_servers', false, false], + ]); + $this->config + ->expects($this->once()) + ->method('getSystemValueString') + ->with('proxy', '') + ->willReturn(''); $this->certificateManager ->expects($this->never()) ->method('listCertificates'); @@ -494,8 +480,8 @@ class ClientTest extends \Test\TestCase { 'on_redirect' => function ( \Psr\Http\Message\RequestInterface $request, \Psr\Http\Message\ResponseInterface $response, - \Psr\Http\Message\UriInterface $uri - ) { + \Psr\Http\Message\UriInterface $uri, + ): void { }, ], ], self::invokePrivate($this->client, 'buildRequestOptions', [[]])); @@ -503,20 +489,24 @@ class ClientTest extends \Test\TestCase { public function testSetDefaultOptionsWithProxy(): void { $this->config - ->expects($this->exactly(4)) + ->expects($this->exactly(2)) + ->method('getSystemValueBool') + ->willReturnMap([ + ['installed', false, true], + ['allow_local_remote_servers', false, false], + ]); + $this->config + ->expects($this->once()) ->method('getSystemValue') - ->withConsecutive( - ['proxy', ''], - ['proxyuserpwd', ''], - ['proxyexclude', []], - ['installed', false], - ) - ->willReturnOnConsecutiveCalls( - 'foo', - '', - [], - true, - ); + ->with('proxyexclude', []) + ->willReturn([]); + $this->config + ->expects($this->exactly(2)) + ->method('getSystemValueString') + ->willReturnMap([ + ['proxy', '', 'foo'], + ['proxyuserpwd', '', ''], + ]); $this->certificateManager ->expects($this->once()) ->method('getAbsoluteBundlePath') @@ -541,8 +531,8 @@ class ClientTest extends \Test\TestCase { 'on_redirect' => function ( \Psr\Http\Message\RequestInterface $request, \Psr\Http\Message\ResponseInterface $response, - \Psr\Http\Message\UriInterface $uri - ) { + \Psr\Http\Message\UriInterface $uri, + ): void { }, ], ], self::invokePrivate($this->client, 'buildRequestOptions', [[]])); @@ -550,20 +540,24 @@ class ClientTest extends \Test\TestCase { public function testSetDefaultOptionsWithProxyAndExclude(): void { $this->config - ->expects($this->exactly(4)) + ->expects($this->exactly(2)) + ->method('getSystemValueBool') + ->willReturnMap([ + ['installed', false, true], + ['allow_local_remote_servers', false, false], + ]); + $this->config + ->expects($this->once()) ->method('getSystemValue') - ->withConsecutive( - ['proxy', ''], - ['proxyuserpwd', ''], - ['proxyexclude', []], - ['installed', false], - ) - ->willReturnOnConsecutiveCalls( - 'foo', - '', - ['bar'], - true, - ); + ->with('proxyexclude', []) + ->willReturn(['bar']); + $this->config + ->expects($this->exactly(2)) + ->method('getSystemValueString') + ->willReturnMap([ + ['proxy', '', 'foo'], + ['proxyuserpwd', '', ''], + ]); $this->certificateManager ->expects($this->once()) ->method('getAbsoluteBundlePath') @@ -589,8 +583,8 @@ class ClientTest extends \Test\TestCase { 'on_redirect' => function ( \Psr\Http\Message\RequestInterface $request, \Psr\Http\Message\ResponseInterface $response, - \Psr\Http\Message\UriInterface $uri - ) { + \Psr\Http\Message\UriInterface $uri, + ): void { }, ], ], self::invokePrivate($this->client, 'buildRequestOptions', [[]])); diff --git a/tests/lib/Http/Client/DnsPinMiddlewareTest.php b/tests/lib/Http/Client/DnsPinMiddlewareTest.php new file mode 100644 index 00000000000..9c0aa198cd8 --- /dev/null +++ b/tests/lib/Http/Client/DnsPinMiddlewareTest.php @@ -0,0 +1,547 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace lib\Http\Client; + +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Response; +use OC\Http\Client\DnsPinMiddleware; +use OC\Http\Client\NegativeDnsCache; +use OC\Memcache\NullCache; +use OC\Net\IpAddressClassifier; +use OCP\Http\Client\LocalServerException; +use OCP\ICacheFactory; +use Psr\Http\Message\RequestInterface; +use Test\TestCase; + +class DnsPinMiddlewareTest extends TestCase { + private DnsPinMiddleware $dnsPinMiddleware; + + protected function setUp(): void { + parent::setUp(); + + $cacheFactory = $this->createMock(ICacheFactory::class); + $cacheFactory + ->method('createLocal') + ->willReturn(new NullCache()); + + $ipAddressClassifier = new IpAddressClassifier(); + $negativeDnsCache = new NegativeDnsCache($cacheFactory); + + $this->dnsPinMiddleware = $this->getMockBuilder(DnsPinMiddleware::class) + ->setConstructorArgs([$negativeDnsCache, $ipAddressClassifier]) + ->onlyMethods(['dnsGetRecord']) + ->getMock(); + } + + public function testPopulateDnsCacheIPv4(): void { + $mockHandler = new MockHandler([ + static function (RequestInterface $request, array $options) { + self::arrayHasKey('curl', $options); + self::arrayHasKey(CURLOPT_RESOLVE, $options['curl']); + self::assertEquals([ + 'www.example.com:80:1.1.1.1', + 'www.example.com:443:1.1.1.1' + ], $options['curl'][CURLOPT_RESOLVE]); + return new Response(200); + }, + ]); + + $this->dnsPinMiddleware + ->method('dnsGetRecord') + ->willReturnCallback(function (string $hostname, int $type) { + // example.com SOA + if ($hostname === 'example.com') { + return match ($type) { + DNS_SOA => [ + [ + 'host' => 'example.com', + 'class' => 'IN', + 'ttl' => 7079, + 'type' => 'SOA', + 'minimum-ttl' => 3600, + ] + ], + }; + } + + // example.com A, AAAA, CNAME + if ($hostname === 'www.example.com') { + return match ($type) { + DNS_A => [], + DNS_AAAA => [], + DNS_CNAME => [ + [ + 'host' => 'www.example.com', + 'class' => 'IN', + 'ttl' => 1800, + 'type' => 'A', + 'target' => 'www.example.net' + ] + ], + }; + } + + // example.net SOA + if ($hostname === 'example.net') { + return match ($type) { + DNS_SOA => [ + [ + 'host' => 'example.net', + 'class' => 'IN', + 'ttl' => 7079, + 'type' => 'SOA', + 'minimum-ttl' => 3600, + ] + ], + }; + } + + // example.net A, AAAA, CNAME + if ($hostname === 'www.example.net') { + return match ($type) { + DNS_A => [ + [ + 'host' => 'www.example.net', + 'class' => 'IN', + 'ttl' => 1800, + 'type' => 'A', + 'ip' => '1.1.1.1' + ] + ], + DNS_AAAA => [], + DNS_CNAME => [], + }; + } + + return false; + }); + + $stack = new HandlerStack($mockHandler); + $stack->push($this->dnsPinMiddleware->addDnsPinning()); + $handler = $stack->resolve(); + + $handler( + new Request('GET', 'https://www.example.com'), + ['nextcloud' => ['allow_local_address' => false]] + ); + } + + public function testPopulateDnsCacheIPv6(): void { + $mockHandler = new MockHandler([ + static function (RequestInterface $request, array $options) { + self::arrayHasKey('curl', $options); + self::arrayHasKey(CURLOPT_RESOLVE, $options['curl']); + self::assertEquals([ + 'www.example.com:80:1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001', + 'www.example.com:443:1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001' + ], $options['curl'][CURLOPT_RESOLVE]); + return new Response(200); + }, + ]); + + $this->dnsPinMiddleware + ->method('dnsGetRecord') + ->willReturnCallback(function (string $hostname, int $type) { + // example.com SOA + if ($hostname === 'example.com') { + return match ($type) { + DNS_SOA => [ + [ + 'host' => 'example.com', + 'class' => 'IN', + 'ttl' => 7079, + 'type' => 'SOA', + 'minimum-ttl' => 3600, + ] + ], + }; + } + + // example.com A, AAAA, CNAME + if ($hostname === 'www.example.com') { + return match ($type) { + DNS_A => [], + DNS_AAAA => [], + DNS_CNAME => [ + [ + 'host' => 'www.example.com', + 'class' => 'IN', + 'ttl' => 1800, + 'type' => 'A', + 'target' => 'www.example.net' + ] + ], + }; + } + + // example.net SOA + if ($hostname === 'example.net') { + return match ($type) { + DNS_SOA => [ + [ + 'host' => 'example.net', + 'class' => 'IN', + 'ttl' => 7079, + 'type' => 'SOA', + 'minimum-ttl' => 3600, + ] + ], + }; + } + + // example.net A, AAAA, CNAME + if ($hostname === 'www.example.net') { + return match ($type) { + DNS_A => [ + [ + 'host' => 'www.example.net', + 'class' => 'IN', + 'ttl' => 1800, + 'type' => 'A', + 'ip' => '1.1.1.1' + ], + [ + 'host' => 'www.example.net', + 'class' => 'IN', + 'ttl' => 1800, + 'type' => 'A', + 'ip' => '1.0.0.1' + ], + ], + DNS_AAAA => [ + [ + 'host' => 'www.example.net', + 'class' => 'IN', + 'ttl' => 1800, + 'type' => 'AAAA', + 'ip' => '2606:4700:4700::1111' + ], + [ + 'host' => 'www.example.net', + 'class' => 'IN', + 'ttl' => 1800, + 'type' => 'AAAA', + 'ip' => '2606:4700:4700::1001' + ], + ], + DNS_CNAME => [], + }; + } + + return false; + }); + + $stack = new HandlerStack($mockHandler); + $stack->push($this->dnsPinMiddleware->addDnsPinning()); + $handler = $stack->resolve(); + + $handler( + new Request('GET', 'https://www.example.com'), + ['nextcloud' => ['allow_local_address' => false]] + ); + } + + public function testAllowLocalAddress(): void { + $mockHandler = new MockHandler([ + static function (RequestInterface $request, array $options) { + self::assertArrayNotHasKey('curl', $options); + return new Response(200); + }, + ]); + + $stack = new HandlerStack($mockHandler); + $stack->push($this->dnsPinMiddleware->addDnsPinning()); + $handler = $stack->resolve(); + + $handler( + new Request('GET', 'https://www.example.com'), + ['nextcloud' => ['allow_local_address' => true]] + ); + } + + public function testRejectIPv4(): void { + $this->expectException(LocalServerException::class); + $this->expectExceptionMessage('violates local access rules'); + + $mockHandler = new MockHandler([ + static function (RequestInterface $request, array $options): void { + // The handler should not be called + }, + ]); + + $this->dnsPinMiddleware + ->method('dnsGetRecord') + ->willReturnCallback(function (string $hostname, int $type) { + return match ($type) { + DNS_SOA => [ + [ + 'host' => 'example.com', + 'class' => 'IN', + 'ttl' => 7079, + 'type' => 'SOA', + 'minimum-ttl' => 3600, + ] + ], + DNS_A => [ + [ + 'host' => 'example.com', + 'class' => 'IN', + 'ttl' => 1800, + 'type' => 'A', + 'ip' => '192.168.0.1' + ] + ], + DNS_AAAA => [], + DNS_CNAME => [], + }; + }); + + $stack = new HandlerStack($mockHandler); + $stack->push($this->dnsPinMiddleware->addDnsPinning()); + $handler = $stack->resolve(); + + $handler( + new Request('GET', 'https://www.example.com'), + ['nextcloud' => ['allow_local_address' => false]] + ); + } + + public function testRejectIPv6(): void { + $this->expectException(LocalServerException::class); + $this->expectExceptionMessage('violates local access rules'); + + $mockHandler = new MockHandler([ + static function (RequestInterface $request, array $options): void { + // The handler should not be called + }, + ]); + + $this->dnsPinMiddleware + ->method('dnsGetRecord') + ->willReturnCallback(function (string $hostname, int $type) { + return match ($type) { + DNS_SOA => [ + [ + 'host' => 'example.com', + 'class' => 'IN', + 'ttl' => 7079, + 'type' => 'SOA', + 'minimum-ttl' => 3600, + ] + ], + DNS_A => [], + DNS_AAAA => [ + [ + 'host' => 'ipv6.example.com', + 'class' => 'IN', + 'ttl' => 1800, + 'type' => 'AAAA', + 'ipv6' => 'fd12:3456:789a:1::1' + ] + ], + DNS_CNAME => [], + }; + }); + + $stack = new HandlerStack($mockHandler); + $stack->push($this->dnsPinMiddleware->addDnsPinning()); + $handler = $stack->resolve(); + + $handler( + new Request('GET', 'https://ipv6.example.com'), + ['nextcloud' => ['allow_local_address' => false]] + ); + } + + public function testRejectCanonicalName(): void { + $this->expectException(LocalServerException::class); + $this->expectExceptionMessage('violates local access rules'); + + $mockHandler = new MockHandler([ + static function (RequestInterface $request, array $options): void { + // The handler should not be called + }, + ]); + + $this->dnsPinMiddleware + ->method('dnsGetRecord') + ->willReturnCallback(function (string $hostname, int $type) { + // example.com SOA + if ($hostname === 'example.com') { + return match ($type) { + DNS_SOA => [ + [ + 'host' => 'example.com', + 'class' => 'IN', + 'ttl' => 7079, + 'type' => 'SOA', + 'minimum-ttl' => 3600, + ] + ], + }; + } + + // example.com A, AAAA, CNAME + if ($hostname === 'www.example.com') { + return match ($type) { + DNS_A => [], + DNS_AAAA => [], + DNS_CNAME => [ + [ + 'host' => 'www.example.com', + 'class' => 'IN', + 'ttl' => 1800, + 'type' => 'A', + 'target' => 'www.example.net' + ] + ], + }; + } + + // example.net SOA + if ($hostname === 'example.net') { + return match ($type) { + DNS_SOA => [ + [ + 'host' => 'example.net', + 'class' => 'IN', + 'ttl' => 7079, + 'type' => 'SOA', + 'minimum-ttl' => 3600, + ] + ], + }; + } + + // example.net A, AAAA, CNAME + if ($hostname === 'www.example.net') { + return match ($type) { + DNS_A => [ + [ + 'host' => 'www.example.net', + 'class' => 'IN', + 'ttl' => 1800, + 'type' => 'A', + 'ip' => '192.168.0.2' + ] + ], + DNS_AAAA => [], + DNS_CNAME => [], + }; + } + + return false; + }); + + $stack = new HandlerStack($mockHandler); + $stack->push($this->dnsPinMiddleware->addDnsPinning()); + $handler = $stack->resolve(); + + $handler( + new Request('GET', 'https://www.example.com'), + ['nextcloud' => ['allow_local_address' => false]] + ); + } + + public function testRejectFaultyResponse(): void { + $this->expectException(LocalServerException::class); + $this->expectExceptionMessage('No DNS record found for www.example.com'); + + $mockHandler = new MockHandler([ + static function (RequestInterface $request, array $options): void { + // The handler should not be called + }, + ]); + + $this->dnsPinMiddleware + ->method('dnsGetRecord') + ->willReturnCallback(function (string $hostname, int $type) { + return false; + }); + + $stack = new HandlerStack($mockHandler); + $stack->push($this->dnsPinMiddleware->addDnsPinning()); + $handler = $stack->resolve(); + + $handler( + new Request('GET', 'https://www.example.com'), + ['nextcloud' => ['allow_local_address' => false]] + ); + } + + public function testIgnoreSubdomainForSoaQuery(): void { + $mockHandler = new MockHandler([ + static function (RequestInterface $request, array $options): void { + // The handler should not be called + }, + ]); + + $dnsQueries = []; + + $this->dnsPinMiddleware + ->method('dnsGetRecord') + ->willReturnCallback(function (string $hostname, int $type) use (&$dnsQueries) { + // log query + $dnsQueries[] = $hostname . $type; + + // example.com SOA + if ($hostname === 'example.com') { + return match ($type) { + DNS_SOA => [ + [ + 'host' => 'example.com', + 'class' => 'IN', + 'ttl' => 7079, + 'type' => 'SOA', + 'minimum-ttl' => 3600, + ] + ], + }; + } + + // example.net A, AAAA, CNAME + if ($hostname === 'subsubdomain.subdomain.example.com') { + return match ($type) { + DNS_A => [ + [ + 'host' => 'subsubdomain.subdomain.example.com', + 'class' => 'IN', + 'ttl' => 1800, + 'type' => 'A', + 'ip' => '1.1.1.1' + ] + ], + DNS_AAAA => [], + DNS_CNAME => [], + }; + } + + return false; + }); + + $stack = new HandlerStack($mockHandler); + $stack->push($this->dnsPinMiddleware->addDnsPinning()); + $handler = $stack->resolve(); + + $handler( + new Request('GET', 'https://subsubdomain.subdomain.example.com'), + ['nextcloud' => ['allow_local_address' => false]] + ); + + $this->assertCount(3, $dnsQueries); + $this->assertContains('example.com' . DNS_SOA, $dnsQueries); + $this->assertContains('subsubdomain.subdomain.example.com' . DNS_A, $dnsQueries); + $this->assertContains('subsubdomain.subdomain.example.com' . DNS_AAAA, $dnsQueries); + // CNAME should not be queried if A or AAAA succeeded already + $this->assertNotContains('subsubdomain.subdomain.example.com' . DNS_CNAME, $dnsQueries); + } +} diff --git a/tests/lib/Http/Client/NegativeDnsCacheTest.php b/tests/lib/Http/Client/NegativeDnsCacheTest.php index b36524acf90..eb0f86f5c7a 100644 --- a/tests/lib/Http/Client/NegativeDnsCacheTest.php +++ b/tests/lib/Http/Client/NegativeDnsCacheTest.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021, Lukas Reschke <lukas@statuscode.ch> - * - * @author Lukas Reschke <lukas@statuscode.ch> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Http\Client; @@ -47,7 +30,7 @@ class NegativeDnsCacheTest extends \Test\TestCase { ->method('createLocal') ->with('NegativeDnsCache') ->willReturn($this->cache); - + $this->negativeDnsCache = new NegativeDnsCache($this->cacheFactory); } @@ -57,16 +40,16 @@ class NegativeDnsCacheTest extends \Test\TestCase { ->method('set') ->with('www.example.com-1', 'true', 3600); - $this->negativeDnsCache->setNegativeCacheForDnsType("www.example.com", DNS_A, 3600); + $this->negativeDnsCache->setNegativeCacheForDnsType('www.example.com', DNS_A, 3600); } - public function testIsNegativeCached() { + public function testIsNegativeCached(): void { $this->cache ->expects($this->once()) ->method('hasKey') ->with('www.example.com-1') ->willReturn(true); - $this->assertTrue($this->negativeDnsCache->isNegativeCached("www.example.com", DNS_A)); + $this->assertTrue($this->negativeDnsCache->isNegativeCached('www.example.com', DNS_A)); } } diff --git a/tests/lib/Http/Client/ResponseTest.php b/tests/lib/Http/Client/ResponseTest.php index 1384e4e732c..1acf1eb1cbd 100644 --- a/tests/lib/Http/Client/ResponseTest.php +++ b/tests/lib/Http/Client/ResponseTest.php @@ -1,9 +1,9 @@ <?php + /** - * Copyright (c) 2015 Lukas Reschke <lukas@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Http\Client; @@ -24,22 +24,22 @@ class ResponseTest extends \Test\TestCase { $this->guzzleResponse = new GuzzleResponse(418); } - public function testGetBody() { + public function testGetBody(): void { $response = new Response($this->guzzleResponse->withBody(Utils::streamFor('MyResponse'))); $this->assertSame('MyResponse', $response->getBody()); } - public function testGetStatusCode() { + public function testGetStatusCode(): void { $response = new Response($this->guzzleResponse); $this->assertSame(418, $response->getStatusCode()); } - public function testGetHeader() { + public function testGetHeader(): void { $response = new Response($this->guzzleResponse->withHeader('bar', 'foo')); $this->assertSame('foo', $response->getHeader('bar')); } - public function testGetHeaders() { + public function testGetHeaders(): void { $response = new Response($this->guzzleResponse ->withHeader('bar', 'foo') ->withHeader('x-awesome', 'yes') |