diff --git a/composer.json b/composer.json index 57fbfaaa35c2b..ecac4ca035edc 100644 --- a/composer.json +++ b/composer.json @@ -76,6 +76,7 @@ "ramsey/uuid": "~3.8.0", "symfony/console": "~4.4.0", "symfony/event-dispatcher": "~4.4.0", + "symfony/http-foundation": "^3.4|^4.0|^5.0", "symfony/process": "~4.4.0", "tedivm/jshrink": "~1.3.0", "tubalmartin/cssmin": "4.1.1", diff --git a/composer.lock b/composer.lock index 8a5d82536cee4..a0cb219fb15b7 100644 --- a/composer.lock +++ b/composer.lock @@ -4558,6 +4558,70 @@ ], "time": "2020-05-20T17:43:50+00:00" }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5e20b83385a77593259c9f8beb2c43cd03b2ac14", + "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-06T08:49:21+00:00" + }, { "name": "symfony/event-dispatcher", "version": "v4.4.10", @@ -4845,6 +4909,81 @@ ], "time": "2020-05-20T17:43:50+00:00" }, + { + "name": "symfony/http-foundation", + "version": "v5.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "1f0d6627e680591c61e9176f04a0dc887b4e6702" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/1f0d6627e680591c61e9176f04a0dc887b4e6702", + "reference": "1f0d6627e680591c61e9176f04a0dc887b4e6702", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.15" + }, + "require-dev": { + "predis/predis": "~1.0", + "symfony/cache": "^4.4|^5.0", + "symfony/expression-language": "^4.4|^5.0", + "symfony/mime": "^4.4|^5.0" + }, + "suggest": { + "symfony/mime": "To use the file extension guesser" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpFoundation Component", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-23T10:04:31+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.18.0", @@ -5089,7 +5228,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -5392,7 +5531,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -7126,20 +7265,6 @@ "constructor", "instantiate" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], "time": "2020-05-29T17:27:14+00:00" }, { @@ -7202,20 +7327,6 @@ "parser", "php" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" - } - ], "time": "2020-05-25T17:44:05+00:00" }, { @@ -10993,145 +11104,6 @@ ], "time": "2020-06-12T08:11:32+00:00" }, - { - "name": "symfony/deprecation-contracts", - "version": "v2.1.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5e20b83385a77593259c9f8beb2c43cd03b2ac14", - "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.1-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-06-06T08:49:21+00:00" - }, - { - "name": "symfony/http-foundation", - "version": "v5.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "f93055171b847915225bd5b0a5792888419d8d75" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f93055171b847915225bd5b0a5792888419d8d75", - "reference": "f93055171b847915225bd5b0a5792888419d8d75", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php80": "^1.15" - }, - "require-dev": { - "predis/predis": "~1.0", - "symfony/cache": "^4.4|^5.0", - "symfony/expression-language": "^4.4|^5.0", - "symfony/mime": "^4.4|^5.0" - }, - "suggest": { - "symfony/mime": "To use the file extension guesser" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony HttpFoundation Component", - "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-06-15T06:52:54+00:00" - }, { "name": "symfony/mime", "version": "v5.1.2", diff --git a/lib/internal/Magento/Framework/App/MaintenanceMode.php b/lib/internal/Magento/Framework/App/MaintenanceMode.php index 11347e4220c26..3fba6cb59eb66 100644 --- a/lib/internal/Magento/Framework/App/MaintenanceMode.php +++ b/lib/internal/Magento/Framework/App/MaintenanceMode.php @@ -6,8 +6,10 @@ namespace Magento\Framework\App; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem; use Magento\Framework\Event\Manager; +use Magento\Framework\HTTP\IpChecker; /** * Application Maintenance Mode @@ -44,14 +46,22 @@ class MaintenanceMode */ private $eventManager; + /** + * @var IpChecker + */ + private $ipChecker; + /** * @param \Magento\Framework\Filesystem $filesystem * @param Manager|null $eventManager + * @param IpChecker|null $ipChecker + * @throws FileSystemException */ - public function __construct(Filesystem $filesystem, ?Manager $eventManager = null) + public function __construct(Filesystem $filesystem, ?Manager $eventManager = null, ?IpChecker $ipChecker = null) { $this->flagDir = $filesystem->getDirectoryWrite(self::FLAG_DIR); $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(Manager::class); + $this->ipChecker = $ipChecker ?: ObjectManager::getInstance()->get(IpChecker::class); } /** @@ -68,7 +78,7 @@ public function isOn($remoteAddr = '') return false; } $info = $this->getAddressInfo(); - return !in_array($remoteAddr, $info); + return $info === [] || !$this->ipChecker->isInRange($remoteAddr, $info); } /** @@ -122,7 +132,7 @@ public function getAddressInfo() { if ($this->flagDir->isExist(self::IP_FILENAME)) { $temp = $this->flagDir->readFile(self::IP_FILENAME); - return explode(',', trim($temp)); + return array_filter(explode(',', trim($temp))); } else { return []; } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php b/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php index 50373c29da2ec..7c2a77489b899 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php @@ -11,6 +11,7 @@ use Magento\Framework\Event\Manager; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\HTTP\IpChecker; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -32,6 +33,11 @@ class MaintenanceModeTest extends TestCase */ private $eventManager; + /** + * @var object + */ + private $ipChecker; + /** * @inheritdoc */ @@ -44,9 +50,11 @@ protected function setup(): void $this->eventManager = $this->createMock(Manager::class); $objectManager = new ObjectManager($this); + $this->ipChecker = $objectManager->getObject(IpChecker::class); $this->model = $objectManager->getObject(MaintenanceMode::class, [ 'filesystem' => $filesystem, 'eventManager' => $this->eventManager, + 'ipChecker' => $this->ipChecker, ]); } @@ -95,7 +103,7 @@ public function testisOnWithIP() $this->flagDir->expects($this->exactly(2)) ->method('isExist') ->willReturnMap($mapisExist); - $this->assertFalse($this->model->isOn()); + $this->assertTrue($this->model->isOn()); } /** @@ -178,7 +186,7 @@ public function testSetAddresses() ->willReturn(''); $this->model->setAddresses(''); - $this->assertEquals([''], $this->model->getAddressInfo()); + $this->assertEquals([], $this->model->getAddressInfo()); } /** @@ -229,13 +237,23 @@ public function testOnSetMultipleAddresses() $this->flagDir->method('readFile') ->with(MaintenanceMode::IP_FILENAME) - ->willReturn('address1,10.50.60.123'); + ->willReturn('address1,10.50.60.123,192.168.0.0/16,2620:0:2d0:200::7/32,1620:0:2d0:200::7'); - $expectedArray = ['address1', '10.50.60.123']; - $this->model->setAddresses('address1,10.50.60.123'); + $expectedArray = ['address1', '10.50.60.123', '192.168.0.0/16', '2620:0:2d0:200::7/32', '1620:0:2d0:200::7']; + $this->model->setAddresses('address1,10.50.60.123,192.168.0.0/16,2620:0:2d0:200::7/32,1620:0:2d0:200::7'); $this->assertEquals($expectedArray, $this->model->getAddressInfo()); - $this->assertFalse($this->model->isOn('address1')); - $this->assertTrue($this->model->isOn('address3')); + $this->assertTrue($this->model->isOn('address1')); // not a valid IPv4 or IPv6 Address + $this->assertTrue($this->model->isOn('address3')); // not a valid IPv4 or IPv6 Address + $this->assertFalse($this->model->isOn('10.50.60.123')); // exact match + $this->assertTrue($this->model->isOn('10.50.60.125')); // exact mismatch + $this->assertFalse($this->model->isOn('192.168.22.1')); // range match + $this->assertTrue($this->model->isOn('192.22.1.1')); // range mismatch + $this->assertTrue($this->model->isOn('address1')); // not an IP address + $this->assertTrue($this->model->isOn('172.16.0.4')); // complete mismatch + $this->assertFalse($this->model->isOn('1620:0:2d0:200::7')); // ipv6 match + $this->assertFalse($this->model->isOn('1620:0:2d0:200:0:0:0:7')); // ipv6 expanded match + $this->assertFalse($this->model->isOn('2620::ff43:0:ff')); // ipv6 range match + $this->assertTrue($this->model->isOn('2720::ff43:0:ff')); // ipv6 range mismatch } /** @@ -256,12 +274,22 @@ public function testOffSetMultipleAddresses() $this->flagDir->method('readFile') ->with(MaintenanceMode::IP_FILENAME) - ->willReturn('address1,10.50.60.123'); + ->willReturn('address1,10.50.60.123,192.168.0.0/16,2620:0:2d0:200::7/32,1620:0:2d0:200::7'); - $expectedArray = ['address1', '10.50.60.123']; - $this->model->setAddresses('address1,10.50.60.123'); + $expectedArray = ['address1', '10.50.60.123', '192.168.0.0/16', '2620:0:2d0:200::7/32', '1620:0:2d0:200::7']; + $this->model->setAddresses('address1,10.50.60.123,192.168.0.0/16,2620:0:2d0:200::7/32,1620:0:2d0:200::7'); $this->assertEquals($expectedArray, $this->model->getAddressInfo()); $this->assertFalse($this->model->isOn('address1')); $this->assertFalse($this->model->isOn('address3')); + $this->assertFalse($this->model->isOn('10.50.60.123')); // exact match + $this->assertFalse($this->model->isOn('10.50.60.125')); // exact mismatch + $this->assertFalse($this->model->isOn('192.168.22.1')); // range match + $this->assertFalse($this->model->isOn('192.22.1.1')); // range mismatch + $this->assertFalse($this->model->isOn('address1')); // not an IP address + $this->assertFalse($this->model->isOn('172.16.0.4')); // complete mismatch + $this->assertFalse($this->model->isOn('1620:0:2d0:200::7')); // ipv6 match + $this->assertFalse($this->model->isOn('1620:0:2d0:200:0:0:0:7')); // ipv6 expanded match + $this->assertFalse($this->model->isOn('2620::ff43:0:ff')); // ipv6 range match + $this->assertFalse($this->model->isOn('2720::ff43:0:ff')); // ipv6 range mismatch } } diff --git a/lib/internal/Magento/Framework/HTTP/IpChecker.php b/lib/internal/Magento/Framework/HTTP/IpChecker.php new file mode 100644 index 0000000000000..a10f8fbc96be9 --- /dev/null +++ b/lib/internal/Magento/Framework/HTTP/IpChecker.php @@ -0,0 +1,29 @@ +validIps[] = $ip; - } elseif ($ip == 'none') { - $this->none[] = $ip; - } else { - $this->invalidIps[] = $ip; + foreach ($ips as $range) { + if ($range === 'none') { + $this->none[] = $range; + continue; + } + + $subnetMask = 32; + $ip = $range; + if (strpos($range, '/') !== false) { + [$ip, $subnetMask] = explode('/', $range); + } + + $ipValidator = new Ip(); + if (!$ipValidator->isValid($ip)) { + $this->invalidIps[] = $range; + continue; + } + + $ipv4Validator = new Ip(['allowipv6' => false]); + $maxBits = $ipv4Validator->isValid($ip) ? 32 : 128; + if ($subnetMask < 0 || $subnetMask > $maxBits) { + $this->invalidIps[] = $range; + continue; } + + $this->validIps[] = $range; } } }