diff --git a/composer.lock b/composer.lock index 6f00fc02..738c3e55 100644 --- a/composer.lock +++ b/composer.lock @@ -357,30 +357,30 @@ }, { "name": "composer/pcre", - "version": "1.0.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560" + "reference": "c8e9d27cfc5ed22643c19c160455b473ffd8aabe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/67a32d7d6f9f560b726ab25a061b38ff3a80c560", - "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560", + "url": "https://api.github.com/repos/composer/pcre/zipball/c8e9d27cfc5ed22643c19c160455b473ffd8aabe", + "reference": "c8e9d27cfc5ed22643c19c160455b473ffd8aabe", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^1.3", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^4.2 || ^5" + "symfony/phpunit-bridge": "^5" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.x-dev" + "dev-main": "2.x-dev" } }, "autoload": { @@ -408,7 +408,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/1.0.1" + "source": "https://github.com/composer/pcre/tree/2.0.0" }, "funding": [ { @@ -424,20 +424,20 @@ "type": "tidelift" } ], - "time": "2022-01-21T20:24:37+00:00" + "time": "2022-02-25T20:05:29+00:00" }, { "name": "composer/semver", - "version": "3.2.9", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "a951f614bd64dcd26137bc9b7b2637ddcfc57649" + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/a951f614bd64dcd26137bc9b7b2637ddcfc57649", - "reference": "a951f614bd64dcd26137bc9b7b2637ddcfc57649", + "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", "shasum": "" }, "require": { @@ -489,7 +489,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.2.9" + "source": "https://github.com/composer/semver/tree/3.3.2" }, "funding": [ { @@ -505,24 +505,24 @@ "type": "tidelift" } ], - "time": "2022-02-04T13:58:43+00:00" + "time": "2022-04-01T19:23:25+00:00" }, { "name": "composer/xdebug-handler", - "version": "3.0.1", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "12f1b79476638a5615ed00ea6adbb269cec96fd8" + "reference": "ced299686f41dce890debac69273b47ffe98a40c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/12f1b79476638a5615ed00ea6adbb269cec96fd8", - "reference": "12f1b79476638a5615ed00ea6adbb269cec96fd8", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", "shasum": "" }, "require": { - "composer/pcre": "^1", + "composer/pcre": "^1 || ^2 || ^3", "php": "^7.2.5 || ^8.0", "psr/log": "^1 || ^2 || ^3" }, @@ -555,7 +555,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.1" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" }, "funding": [ { @@ -571,7 +571,7 @@ "type": "tidelift" } ], - "time": "2022-01-04T18:29:42+00:00" + "time": "2022-02-25T21:32:43+00:00" }, { "name": "dnoegel/php-xdg-base-dir", @@ -612,29 +612,30 @@ }, { "name": "doctrine/instantiator", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^8.0", + "doctrine/coding-standard": "^9", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.22" }, "type": "library", "autoload": { @@ -661,7 +662,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + "source": "https://github.com/doctrine/instantiator/tree/1.4.1" }, "funding": [ { @@ -677,7 +678,7 @@ "type": "tidelift" } ], - "time": "2020-11-10T18:47:58+00:00" + "time": "2022-03-03T08:28:38+00:00" }, { "name": "felixfbecker/advanced-json-rpc", @@ -726,16 +727,16 @@ }, { "name": "felixfbecker/language-server-protocol", - "version": "1.5.1", + "version": "v1.5.2", "source": { "type": "git", "url": "https://github.com/felixfbecker/php-language-server-protocol.git", - "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730" + "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/9d846d1f5cf101deee7a61c8ba7caa0a975cd730", - "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842", + "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842", "shasum": "" }, "require": { @@ -776,9 +777,9 @@ ], "support": { "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", - "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/1.5.1" + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2" }, - "time": "2021-02-22T14:02:09+00:00" + "time": "2022-03-02T22:36:06+00:00" }, { "name": "http-interop/http-factory-tests", @@ -935,25 +936,29 @@ }, { "name": "myclabs/deep-copy", - "version": "1.10.2", + "version": "1.11.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", "autoload": { @@ -978,7 +983,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" }, "funding": [ { @@ -986,7 +991,7 @@ "type": "tidelift" } ], - "time": "2020-11-13T09:40:50+00:00" + "time": "2022-03-03T13:19:32+00:00" }, { "name": "netresearch/jsonmapper", @@ -1041,16 +1046,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.13.2", + "version": "v4.14.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077" + "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1", + "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1", "shasum": "" }, "require": { @@ -1091,9 +1096,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0" }, - "time": "2021-11-30T19:35:32+00:00" + "time": "2022-05-31T20:59:12+00:00" }, { "name": "openlss/lib-array2xml", @@ -1430,16 +1435,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.6.0", + "version": "1.6.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706" + "reference": "77a32518733312af16a44300404e945338981de3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/93ebd0014cab80c4ea9f5e297ea48672f1b87706", - "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", + "reference": "77a32518733312af16a44300404e945338981de3", "shasum": "" }, "require": { @@ -1474,9 +1479,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" }, - "time": "2022-01-04T19:58:01+00:00" + "time": "2022-03-15T21:29:03+00:00" }, { "name": "phpspec/prophecy", @@ -1599,16 +1604,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.11", + "version": "9.2.15", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "665a1ac0a763c51afc30d6d130dac0813092b17f" + "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/665a1ac0a763c51afc30d6d130dac0813092b17f", - "reference": "665a1ac0a763c51afc30d6d130dac0813092b17f", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", "shasum": "" }, "require": { @@ -1664,7 +1669,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.11" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" }, "funding": [ { @@ -1672,7 +1677,7 @@ "type": "github" } ], - "time": "2022-02-18T12:46:09+00:00" + "time": "2022-03-07T09:28:20+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1917,16 +1922,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.14", + "version": "9.5.21", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "1883687169c017d6ae37c58883ca3994cfc34189" + "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1883687169c017d6ae37c58883ca3994cfc34189", - "reference": "1883687169c017d6ae37c58883ca3994cfc34189", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e32b76be457de00e83213528f6bb37e2a38fcb1", + "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1", "shasum": "" }, "require": { @@ -1942,7 +1947,7 @@ "phar-io/version": "^3.0.2", "php": ">=7.3", "phpspec/prophecy": "^1.12.1", - "phpunit/php-code-coverage": "^9.2.7", + "phpunit/php-code-coverage": "^9.2.13", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.3", @@ -1956,11 +1961,10 @@ "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^2.3.4", + "sebastian/type": "^3.0", "sebastian/version": "^3.0.2" }, "require-dev": { - "ext-pdo": "*", "phpspec/prophecy-phpunit": "^2.0.1" }, "suggest": { @@ -2004,7 +2008,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.14" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.21" }, "funding": [ { @@ -2016,7 +2020,7 @@ "type": "github" } ], - "time": "2022-02-18T12:54:07+00:00" + "time": "2022-06-19T12:14:25+00:00" }, { "name": "psalm/plugin-phpunit", @@ -2542,16 +2546,16 @@ }, { "name": "sebastian/environment", - "version": "5.1.3", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", "shasum": "" }, "require": { @@ -2593,7 +2597,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" }, "funding": [ { @@ -2601,7 +2605,7 @@ "type": "github" } ], - "time": "2020-09-28T05:52:38+00:00" + "time": "2022-04-03T09:37:03+00:00" }, { "name": "sebastian/exporter", @@ -3033,28 +3037,28 @@ }, { "name": "sebastian/type", - "version": "2.3.4", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914" + "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8cd8a1c753c90bc1a0f5372170e3e489136f914", - "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", "shasum": "" }, "require": { "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -3077,7 +3081,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/2.3.4" + "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" }, "funding": [ { @@ -3085,7 +3089,7 @@ "type": "github" } ], - "time": "2021-06-15T12:49:02+00:00" + "time": "2022-03-15T09:54:48+00:00" }, { "name": "sebastian/version", @@ -3225,16 +3229,16 @@ }, { "name": "symfony/console", - "version": "v5.4.3", + "version": "v5.4.9", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a2a86ec353d825c75856c6fd14fac416a7bdb6b8" + "reference": "829d5d1bf60b2efeb0887b7436873becc71a45eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a2a86ec353d825c75856c6fd14fac416a7bdb6b8", - "reference": "a2a86ec353d825c75856c6fd14fac416a7bdb6b8", + "url": "https://api.github.com/repos/symfony/console/zipball/829d5d1bf60b2efeb0887b7436873becc71a45eb", + "reference": "829d5d1bf60b2efeb0887b7436873becc71a45eb", "shasum": "" }, "require": { @@ -3304,7 +3308,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.3" + "source": "https://github.com/symfony/console/tree/v5.4.9" }, "funding": [ { @@ -3320,20 +3324,20 @@ "type": "tidelift" } ], - "time": "2022-01-26T16:28:35+00:00" + "time": "2022-05-18T06:17:34+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", "shasum": "" }, "require": { @@ -3371,7 +3375,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1" }, "funding": [ { @@ -3387,20 +3391,20 @@ "type": "tidelift" } ], - "time": "2021-07-12T14:48:14+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "30885182c981ab175d4d034db0f6f469898070ab" + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", - "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", "shasum": "" }, "require": { @@ -3415,7 +3419,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3423,12 +3427,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3453,7 +3457,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" }, "funding": [ { @@ -3469,20 +3473,20 @@ "type": "tidelift" } ], - "time": "2021-10-20T20:35:02+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" + "reference": "433d05519ce6990bf3530fba6957499d327395c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", - "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2", + "reference": "433d05519ce6990bf3530fba6957499d327395c2", "shasum": "" }, "require": { @@ -3494,7 +3498,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3534,7 +3538,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0" }, "funding": [ { @@ -3550,20 +3554,20 @@ "type": "tidelift" } ], - "time": "2021-11-23T21:10:46+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", "shasum": "" }, "require": { @@ -3575,7 +3579,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3618,7 +3622,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" }, "funding": [ { @@ -3634,20 +3638,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", "shasum": "" }, "require": { @@ -3662,7 +3666,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3701,7 +3705,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" }, "funding": [ { @@ -3717,20 +3721,20 @@ "type": "tidelift" } ], - "time": "2021-11-30T18:21:41+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" + "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", - "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/e440d35fa0286f77fb45b79a03fedbeda9307e85", + "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85", "shasum": "" }, "require": { @@ -3739,7 +3743,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3780,7 +3784,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.26.0" }, "funding": [ { @@ -3796,20 +3800,20 @@ "type": "tidelift" } ], - "time": "2021-06-05T21:20:04+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9" + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/57b712b08eddb97c762a8caa32c84e037892d2e9", - "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", "shasum": "" }, "require": { @@ -3818,7 +3822,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3863,7 +3867,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" }, "funding": [ { @@ -3879,26 +3883,26 @@ "type": "tidelift" } ], - "time": "2021-09-13T13:58:33+00:00" + "time": "2022-05-10T07:21:04+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc" + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/24d9dc654b83e91aa59f9d167b131bc3b5bea24c", + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c", "shasum": "" }, "require": { "php": ">=7.2.5", "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1" + "symfony/deprecation-contracts": "^2.1|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -3946,7 +3950,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.1" }, "funding": [ { @@ -3962,20 +3966,20 @@ "type": "tidelift" } ], - "time": "2021-11-04T16:48:04+00:00" + "time": "2022-03-13T20:07:29+00:00" }, { "name": "symfony/string", - "version": "v5.4.3", + "version": "v5.4.9", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "92043b7d8383e48104e411bc9434b260dbeb5a10" + "reference": "985e6a9703ef5ce32ba617c9c7d97873bb7b2a99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/92043b7d8383e48104e411bc9434b260dbeb5a10", - "reference": "92043b7d8383e48104e411bc9434b260dbeb5a10", + "url": "https://api.github.com/repos/symfony/string/zipball/985e6a9703ef5ce32ba617c9c7d97873bb7b2a99", + "reference": "985e6a9703ef5ce32ba617c9c7d97873bb7b2a99", "shasum": "" }, "require": { @@ -3997,12 +4001,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\String\\": "" - }, "files": [ "Resources/functions.php" ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, "exclude-from-classmap": [ "/Tests/" ] @@ -4032,7 +4036,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.3" + "source": "https://github.com/symfony/string/tree/v5.4.9" }, "funding": [ { @@ -4048,7 +4052,7 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2022-04-19T10:40:37+00:00" }, { "name": "theseer/tokenizer", @@ -4102,16 +4106,16 @@ }, { "name": "vimeo/psalm", - "version": "4.21.0", + "version": "4.23.0", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "d8bec4c7aaee111a532daec32fb09de5687053d1" + "reference": "f1fe6ff483bf325c803df9f510d09a03fd796f88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/d8bec4c7aaee111a532daec32fb09de5687053d1", - "reference": "d8bec4c7aaee111a532daec32fb09de5687053d1", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/f1fe6ff483bf325c803df9f510d09a03fd796f88", + "reference": "f1fe6ff483bf325c803df9f510d09a03fd796f88", "shasum": "" }, "require": { @@ -4136,6 +4140,7 @@ "php": "^7.1|^8", "sebastian/diff": "^3.0 || ^4.0", "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0 || ^6.0", + "symfony/polyfill-php80": "^1.25", "webmozart/path-util": "^2.3" }, "provide": { @@ -4202,27 +4207,27 @@ ], "support": { "issues": "https://github.com/vimeo/psalm/issues", - "source": "https://github.com/vimeo/psalm/tree/4.21.0" + "source": "https://github.com/vimeo/psalm/tree/4.23.0" }, - "time": "2022-02-18T04:34:15+00:00" + "time": "2022-04-28T17:35:49+00:00" }, { "name": "webmozart/assert", - "version": "1.10.0", + "version": "1.11.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" + "ext-ctype": "*", + "php": "^7.2 || ^8.0" }, "conflict": { "phpstan/phpstan": "<0.12.20", @@ -4260,9 +4265,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" + "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, - "time": "2021-03-09T10:59:23+00:00" + "time": "2022-06-03T18:03:27+00:00" }, { "name": "webmozart/path-util", diff --git a/docs/book/v2/api.md b/docs/book/v2/api.md index c608702e..6f57e8b7 100644 --- a/docs/book/v2/api.md +++ b/docs/book/v2/api.md @@ -104,10 +104,10 @@ $jsonResponse = new JsonResponse($data, 422, [ ## ServerRequestFactory -This static class can be used to marshal a `ServerRequest` instance from the PHP environment. The -primary entry point is `Laminas\Diactoros\ServerRequestFactory::fromGlobals(array $server, array -$query, array $body, array $cookies, array $files)`. This method will create a new `ServerRequest` -instance with the data provided. Examples of usage are: +This static class can be used to marshal a `ServerRequest` instance from the PHP environment. +The primary entry point is `Laminas\Diactoros\ServerRequestFactory::fromGlobals(array $server, array $query, array $body, array $cookies, array $files, ?Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface $requestFilter)`. +This method will create a new `ServerRequest` instance with the data provided. +Examples of usage are: ```php // Returns new ServerRequest instance, using values from superglobals: @@ -124,8 +124,22 @@ $request = ServerRequestFactory::fromGlobals( $_COOKIE, $_FILES ); + +### Request Filters + +Since version 2.11.1, this method takes the additional optional argument `$requestFilter`. +This should be a `null` value, or an instance of [`Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface`](server-request-filters.md). +For version 2 releases, if a `null` is provided, internally the method will assign a [`Laminas\Diactoros\ServerRequestFilter\FilerUsingXForwardedHeaders`](server-request-filters.md#filterusingxforwardedheaders) instance configured as follows: + +```php +$requestFilter = $requestFilter ?: FilterUsingXForwardedHeaders::trustReservedSubnets(); ``` +The request filter is called on the generated server request instance, and its result is returned from `fromGlobals()`. + +**For version 3 releases, this method will switch to using a `Laminas\Diactoros\ServerRequestFilter\DoNotFilter` by default.** +If you are using this factory method directly, please be aware and update your code accordingly. + ### ServerRequestFactory Helper Functions In order to create the various artifacts required by a `ServerRequest` instance, @@ -137,8 +151,10 @@ and even the `Cookie` header. These include: (its main purpose is to aggregate the `Authorization` header in the SAPI params when under Apache) - `Laminas\Diactoros\marshalProtocolVersionFromSapi(array $server) : string` -- `Laminas\Diactoros\marshalMethodFromSapi(array $server) : string` -- `Laminas\Diactoros\marshalUriFromSapi(array $server, array $headers) : Uri` +- `Laminas\Diactoros\marshalMethodFromSapi(array $server) : string`. +- `Laminas\Diactoros\marshalUriFromSapi(array $server, array $headers) : Uri`. + Please note: **this function is deprecated as of version 2.11.1**, and no longer used in `ServerRequestFactory::fromGlobals()`. + Use `ServerRequestFactory::fromGlobals()` instead. - `Laminas\Diactoros\marshalHeadersFromSapi(array $server) : array` - `Laminas\Diactoros\parseCookieHeader(string $header) : array` - `Laminas\Diactoros\createUploadedFile(array $spec) : UploadedFile` (creates the diff --git a/docs/book/v2/forward-migration.md b/docs/book/v2/forward-migration.md new file mode 100644 index 00000000..375ad512 --- /dev/null +++ b/docs/book/v2/forward-migration.md @@ -0,0 +1,20 @@ +# Preparing for Version 3 + +## ServerRequestFilterInterface defaults + +Introduced in version 2.11.1, the `Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface` is used by `ServerRequestFactory::fromGlobals()` to allow modifying the generated `ServerRequest` instance prior to returning it. +The primary use case is to allow modifying the generated URI based on the presence of headers such as `X-Forwarded-Host`. +When operating behind a reverse proxy, the `Host` header is often rewritten to the name of the node to which the request is being forwarded, and an `X-Forwarded-Host` header is generated with the original `Host` value to allow the server to determine the original host the request was intended for. +(We have always examined the `X-Forwarded-Proto` header; as of 2.11.1, we also examine the `X-Forwarded-Port` header.) + +To accommodate this use case, we created `Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders`. + +Due to potential security issues, it is generally best to only accept these headers if you trust the reverse proxy that has initiated the request. +(This value is found in `$_SERVER['REMOTE_ADDR']`, which is present as `$request->getServerParams()['REMOTE_ADDR']` within PSR-7 implementations.) +`FilterUsingXForwardedHeaders` provides named constructors to allow you to trust these headers from any source (which has been the default behavior of Diactoros since the beginning), or to specify specific IP addresses or CIDR subnets to trust, along with which headers are trusted. +To prevent backwards compatibility breaks, we use this filter by default, marked to trust **only proxies on private subnets**. + +Features will be added to the 3.11.0 version of [mezzio/mezzio](https://github.com/mezzio/mezzio) that will allow configuring the `Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface` instance, and we recommend explicitly configuring this to utilize the `FilterUsingXForwardedHeaders` if you depend on this functionality. +If you **do not** need the functionality, we recommend specifying `Laminas\Diactoros\ServerRequestFilter\DoNotFilter` as the configured `FilterServerRequestInterface` in your application immediately. + +We will update this documentation with a link to the related functionality in mezzio/mezzio when it is published. diff --git a/docs/book/v2/server-request-filters.md b/docs/book/v2/server-request-filters.md new file mode 100644 index 00000000..30eb2356 --- /dev/null +++ b/docs/book/v2/server-request-filters.md @@ -0,0 +1,112 @@ +# Server Request Filters + +INFO: **New Feature** +Available since version 2.11.1 + +Server request filters allow you to modify the initial state of a generated `ServerRequest` instance as returned from `Laminas\Diactoros\ServerRequestFactory::fromGlobals()`. +Common use cases include: + +- Generating and injecting a request ID. +- Modifying the request URI based on headers provided (e.g., based on the `X-Forwarded-Host` or `X-Forwarded-Proto` headers). + +## FilerServerRequestInterface + +A request filter implements `Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface`: + +```php +namespace Laminas\Diactoros\ServerRequestFilter; + +use Psr\Http\Message\ServerRequestInterface; + +interface FilterServerRequestInterface +{ + public function __invoke(ServerRequestInterface $request): ServerRequestInterface; +} +``` + +## Implementations + +We provide the following implementations: + +- `DoNotFilter`: returns the provided `$request` verbatim. +- `FilterUsingXForwardedHeaders`: if the originating request comes from a trusted proxy, examines the `X-Forwarded-*` headers, and returns the request instance with a URI instance that reflects those headers. + +### DoNotFilter + +This filter returns the `$request` argument back verbatim when invoked. + +### FilterUsingXForwardedHeaders + +Servers behind a reverse proxy need mechanisms to determine the original URL requested. +As such, reverse proxies have provided a number of mechanisms for delivering this information, with the use of `X-Forwarded-*` headers being the most prevalant. +These include: + +- `X-Forwarded-Host`: the original `Host` header value. +- `X-Forwarded-Port`: the original port included in the `Host` header value. +- `X-Forwarded-Proto`: the original URI scheme used to make the request (e.g., "http" or "https"). + +`Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders` provides named constructors for choosing whether to never trust proxies, always trust proxies, or choose wich proxies and/or headers to trust in order to modify the URI composed in the request instance to match the original request. +These named constructors are: + +- `FilterUsingXForwardedHeadersFactory::trustProxies(string[] $proxyCIDRList, string[] $trustedHeaders = FilterUsingXForwardedHeaders::X_FORWARDED_HEADERS): void`: when this method is called, only requests originating from the trusted proxy/ies will be considered, as well as only the headers specified. + Proxies may be specified by IP address, or using [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) for subnets; both IPv4 and IPv6 are accepted. + The special string "*" will be translated to two entries, `0.0.0.0/0` and `::/0`. +- `FilterUsingXForwardedHeaders::trustAny(): void`: when this method is called, the filter will trust requests from any origin, and use any of the above headers to modify the URI instance. + It is functionally equivalent to `FilterUsingXForwardedHeaders::trustProxies(['*'])`. +- `FilterUsingXForwardedHeaders::trustReservedSubnets(): void`: when this method is called, the filter will trust requests made from reserved, private subnets. + It is functionally equivalent to `FilterUsingXForwardedHeaders::trustProxies()` with the following elements in the `$proxyCIDRList`: + - 10.0.0.0/8 + - 127.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - ::1/128 (IPv6 localhost) + - fc00::/7 (IPv6 private networks) + - fe80::/10 (IPv6 local-link addresses) + +Internally, the filter checks the `REMOTE_ADDR` server parameter (as retrieved from `getServerParams()`) and compares it against each proxy listed; the first to match indicates trust. + +#### Constants + +The `FilterUsingXForwardedHeaders` defines the following constants for use in specifying various headers: + +- `HEADER_HOST`: corresponds to `X-Forwarded-Host`. +- `HEADER_PORT`: corresponds to `X-Forwarded-Port`. +- `HEADER_PROTO`: corresponds to `X-Forwarded-Proto`. + +#### Example usage + +Trusting all `X-Forwarded-*` headers from any source: + +```php +$filter = FilterUsingXForwardedHeaders::trustAny(); +``` + +Trusting only the `X-Forwarded-Host` header from any source: + +```php +$filter = FilterUsingXForwardedHeaders::trustProxies('0.0.0.0/0', [FilterUsingXForwardedHeaders::HEADER_HOST]); +``` + +Trusting the `X-Forwarded-Host` and `X-Forwarded-Proto` headers from a single Class C subnet: + +```php +$filter = FilterUsingXForwardedHeaders::trustProxies( + '192.168.1.0/24', + [FilterUsingXForwardedHeaders::HEADER_HOST, FilterUsingXForwardedHeaders::HEADER_PROTO] +); +``` + +Trusting the `X-Forwarded-Host` header from either a Class A or a Class C subnet: + +```php +$filter = FilterUsingXForwardedHeaders::trustProxies( + ['10.1.1.0/16', '192.168.1.0/24'], + [FilterUsingXForwardedHeaders::HEADER_HOST, FilterUsingXForwardedHeaders::HEADER_PROTO] +); +``` + +Trusting any `X-Forwarded-*` header from any private subnet: + +```php +$filter = FilterUsingXForwardedHeaders::trustReservedSubnets(); +``` diff --git a/mkdocs.yml b/mkdocs.yml index cc351e7a..7d41499c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,11 +14,13 @@ nav: - Usage: v2/usage.md - Reference: - Factories: v2/factories.md + - "Server Request Filters": v2/server-request-filters.md - "Custom Responses": v2/custom-responses.md - Serialization: v2/serialization.md - API: v2/api.md - Migration: - "Migration to Version 2": v2/migration.md + - "Preparing for Version 3": v2/forward-migration.md - v1: - Overview: v1/overview.md - Installation: v1/install.md diff --git a/psalm-baseline.xml b/psalm-baseline.xml index fc5a8baf..21ba7271 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + null|callable @@ -242,13 +242,45 @@ + $headers['cookie'] + + $iisUrlRewritten + $requestUri + $unencodedUrl + + + array{string, int|null} + + + $defaults + + + ServerRequest + is_callable(self::$apacheRequestHeaders) + + + getHeaderLine + getServerParams + getUri + withHost + withPort + withScheme + withUri + + + $proxyCIDRList + + + list<non-empty-string> + + diff --git a/psalm.xml.dist b/psalm.xml.dist index 9cee6e9e..0a854e54 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -15,11 +15,26 @@ + + + + + + + + + + + + + + + diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 32510d1f..78afa047 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -13,6 +13,11 @@ class ConfigProvider { + public const CONFIG_KEY = 'laminas-diactoros'; + public const X_FORWARDED = 'x-forwarded-request-filter'; + public const X_FORWARDED_TRUSTED_PROXIES = 'trusted-proxies'; + public const X_FORWARDED_TRUSTED_HEADERS = 'trusted-headers'; + /** * Retrieve configuration for laminas-diactoros. * @@ -22,6 +27,7 @@ public function __invoke() : array { return [ 'dependencies' => $this->getDependencies(), + self::CONFIG_KEY => $this->getComponentConfig(), ]; } @@ -31,6 +37,7 @@ public function __invoke() : array */ public function getDependencies() : array { + // @codingStandardsIgnoreStart return [ 'invokables' => [ RequestFactoryInterface::class => RequestFactory::class, @@ -41,5 +48,16 @@ public function getDependencies() : array UriFactoryInterface::class => UriFactory::class ], ]; + // @codingStandardsIgnoreEnd + } + + public function getComponentConfig(): array + { + return [ + self::X_FORWARDED => [ + self::X_FORWARDED_TRUSTED_PROXIES => '', + self::X_FORWARDED_TRUSTED_HEADERS => [], + ], + ]; } } diff --git a/src/Exception/InvalidForwardedHeaderNameException.php b/src/Exception/InvalidForwardedHeaderNameException.php new file mode 100644 index 00000000..1c446439 --- /dev/null +++ b/src/Exception/InvalidForwardedHeaderNameException.php @@ -0,0 +1,24 @@ +withScheme($https ? 'https' : 'http'); + + // Set the host + [$host, $port] = self::marshalHostAndPort($server); + if (! empty($host)) { + $uri = $uri->withHost($host); + if (! empty($port)) { + $uri = $uri->withPort($port); + } + } + + // URI path + $path = self::marshalRequestPath($server); + + // Strip query string + $path = explode('?', $path, 2)[0]; + + // URI query + $query = ''; + if (isset($server['QUERY_STRING'])) { + $query = ltrim((string) $server['QUERY_STRING'], '?'); + } + + // URI fragment + $fragment = ''; + if (strpos($path, '#') !== false) { + [$path, $fragment] = explode('#', $path, 2); + } + + return $uri + ->withPath($path) + ->withFragment($fragment) + ->withQuery($query); + } + + /** + * Marshal the host and port from the PHP environment. + * + * @return array{string, int|null} Array of two items, host and port, + * in that order (can be passed to a list() operation). + */ + private static function marshalHostAndPort(array $server) : array + { + static $defaults = ['', null]; + + if (! isset($server['SERVER_NAME'])) { + return $defaults; + } + + $host = (string) $server['SERVER_NAME']; + $port = isset($server['SERVER_PORT']) ? (int) $server['SERVER_PORT'] : null; + + if (! isset($server['SERVER_ADDR']) + || ! preg_match('/^\[[0-9a-fA-F\:]+\]$/', $host) + ) { + return [$host, $port]; + } + + // Misinterpreted IPv6-Address + // Reported for Safari on Windows + return self::marshalIpv6HostAndPort($server, $port); + } + + /** + * @return array{string, int|null} Array of two items, host and port, + * in that order (can be passed to a list() operation). + */ + private static function marshalIpv6HostAndPort(array $server, ?int $port) : array + { + $host = '[' . (string) $server['SERVER_ADDR'] . ']'; + $port = $port ?: 80; + $portSeparatorPos = strrpos($host, ':'); + + if (false === $portSeparatorPos) { + return [$host, $port]; + } + + if ($port . ']' === substr($host, $portSeparatorPos + 1)) { + // The last digit of the IPv6-Address has been taken as port + // Unset the port so the default port can be used + $port = null; + } + return [$host, $port]; + } + + /** + * Detect the path for the request + * + * Looks at a variety of criteria in order to attempt to autodetect the base + * request path, including: + * + * - IIS7 UrlRewrite environment + * - REQUEST_URI + * - ORIG_PATH_INFO + */ + private static function marshalRequestPath(array $server) : string + { + // IIS7 with URL Rewrite: make sure we get the unencoded url + // (double slash problem). + $iisUrlRewritten = $server['IIS_WasUrlRewritten'] ?? null; + $unencodedUrl = $server['UNENCODED_URL'] ?? ''; + if ('1' === $iisUrlRewritten && is_string($unencodedUrl) && '' !== $unencodedUrl) { + return $unencodedUrl; + } + + $requestUri = $server['REQUEST_URI'] ?? null; + + if (is_string($requestUri)) { + return preg_replace('#^[^/:]+://[^/]+#', '', $requestUri); + } + + $origPathInfo = $server['ORIG_PATH_INFO'] ?? ''; + if (! is_string($origPathInfo) || '' === $origPathInfo) { + return '/'; + } + + return $origPathInfo; + } + + /** + * @param mixed $https + */ + private static function marshalHttpsValue($https) : bool + { + if (is_bool($https)) { + return $https; + } + + if (! is_string($https)) { + throw new Exception\InvalidArgumentException(sprintf( + 'SAPI HTTPS value MUST be a string or boolean; received %s', + gettype($https) + )); + } + + return 'on' === strtolower($https); + } } diff --git a/src/ServerRequestFilter/DoNotFilter.php b/src/ServerRequestFilter/DoNotFilter.php new file mode 100644 index 00000000..7a6867a8 --- /dev/null +++ b/src/ServerRequestFilter/DoNotFilter.php @@ -0,0 +1,15 @@ + + */ + private $trustedHeaders; + + /** @var list */ + private $trustedProxies; + + /** + * Only allow construction via named constructors + * + * @param list $trustedProxies + * @param list $trustedHeaders + */ + private function __construct( + array $trustedProxies = [], + array $trustedHeaders = [] + ) { + $this->trustedProxies = $trustedProxies; + $this->trustedHeaders = $trustedHeaders; + } + + public function __invoke(ServerRequestInterface $request): ServerRequestInterface + { + $remoteAddress = $request->getServerParams()['REMOTE_ADDR'] ?? ''; + + if ('' === $remoteAddress || ! is_string($remoteAddress)) { + // Should we trigger a warning here? + return $request; + } + + if (! $this->isFromTrustedProxy($remoteAddress)) { + // Do nothing + return $request; + } + + // Update the URI based on the trusted headers + $uri = $originalUri = $request->getUri(); + foreach ($this->trustedHeaders as $headerName) { + $header = $request->getHeaderLine($headerName); + if ('' === $header || false !== strpos($header, ',')) { + // Reject empty headers and/or headers with multiple values + continue; + } + + switch ($headerName) { + case self::HEADER_HOST: + $uri = $uri->withHost($header); + break; + case self::HEADER_PORT: + $uri = $uri->withPort((int) $header); + break; + case self::HEADER_PROTO: + $uri = $uri->withScheme($header); + break; + } + } + + if ($uri !== $originalUri) { + return $request->withUri($uri); + } + + return $request; + } + + /** + * Indicate which proxies and which X-Forwarded headers to trust. + * + * @param list $proxyCIDRList Each element may + * be an IP address or a subnet specified using CIDR notation; both IPv4 + * and IPv6 are supported. The special string "*" will be translated to + * two entries, "0.0.0.0/0" and "::/0". An empty list indicates no + * proxies are trusted. + * @param list $trustedHeaders If + * the list is empty, all X-Forwarded headers are trusted. + * @throws InvalidProxyAddressException + * @throws InvalidForwardedHeaderNameException + */ + public static function trustProxies( + array $proxyCIDRList, + array $trustedHeaders = self::X_FORWARDED_HEADERS + ): self { + $proxyCIDRList = self::normalizeProxiesList($proxyCIDRList); + self::validateTrustedHeaders($trustedHeaders); + + return new self($proxyCIDRList, $trustedHeaders); + } + + /** + * Trust any X-FORWARDED-* headers from any address. + * + * This is functionally equivalent to calling `trustProxies(['*'])`. + * + * WARNING: Only do this if you know for certain that your application + * sits behind a trusted proxy that cannot be spoofed. This should only + * be the case if your server is not publicly addressable, and all requests + * are routed via a reverse proxy (e.g., a load balancer, a server such as + * Caddy, when using Traefik, etc.). + */ + public static function trustAny(): self + { + return self::trustProxies(['*']); + } + + /** + * Trust X-Forwarded headers from reserved subnetworks. + * + * This is functionally equivalent to calling `trustProxies()` where the + * `$proxcyCIDRList` argument is a list with the following: + * + * - 10.0.0.0/8 + * - 127.0.0.0/8 + * - 172.16.0.0/12 + * - 192.168.0.0/16 + * - ::1/128 (IPv6 localhost) + * - fc00::/7 (IPv6 private networks) + * - fe80::/10 (IPv6 local-link addresses) + * + * @param list $trustedHeaders If + * the list is empty, all X-Forwarded headers are trusted. + * @throws InvalidForwardedHeaderNameException + */ + public static function trustReservedSubnets(array $trustedHeaders = self::X_FORWARDED_HEADERS): self + { + return self::trustProxies([ + '10.0.0.0/8', + '127.0.0.0/8', + '172.16.0.0/12', + '192.168.0.0/16', + '::1/128', // ipv6 localhost + 'fc00::/7', // ipv6 private networks + 'fe80::/10', // ipv6 local-link addresses + ], $trustedHeaders); + } + + private function isFromTrustedProxy(string $remoteAddress): bool + { + foreach ($this->trustedProxies as $proxy) { + if (IPRange::matches($remoteAddress, $proxy)) { + return true; + } + } + + return false; + } + + /** @throws InvalidForwardedHeaderNameException */ + private static function validateTrustedHeaders(array $headers): void + { + foreach ($headers as $header) { + if (! in_array($header, self::X_FORWARDED_HEADERS, true)) { + throw InvalidForwardedHeaderNameException::forHeader($header); + } + } + } + + /** + * @param list $proxyCIDRList + * @return list + * @throws InvalidProxyAddressException + */ + private static function normalizeProxiesList(array $proxyCIDRList): array + { + $foundWildcard = false; + + foreach ($proxyCIDRList as $index => $cidr) { + if ($cidr === '*') { + unset($proxyCIDRList[$index]); + $foundWildcard = true; + continue; + } + + if (! self::validateProxyCIDR($cidr)) { + throw InvalidProxyAddressException::forAddress($cidr); + } + } + + if ($foundWildcard) { + $proxyCIDRList[] = '0.0.0.0/0'; + $proxyCIDRList[] = '::/0'; + } + + return $proxyCIDRList; + } + + /** + * @param mixed $cidr + */ + private static function validateProxyCIDR($cidr): bool + { + if (! is_string($cidr) || '' === $cidr) { + return false; + } + + $address = $cidr; + $mask = null; + if (false !== strpos($cidr, '/')) { + [$address, $mask] = explode('/', $cidr, 2); + $mask = (int) $mask; + } + + if (false !== strpos($address, ':')) { + // is IPV6 + return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) + && ( + $mask === null + || ( + $mask <= 128 + && $mask >= 0 + ) + ); + } + + // is IPV4 + return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) + && ( + $mask === null + || ( + $mask <= 32 + && $mask >= 0 + ) + ); + } +} diff --git a/src/ServerRequestFilter/IPRange.php b/src/ServerRequestFilter/IPRange.php new file mode 100644 index 00000000..871dde84 --- /dev/null +++ b/src/ServerRequestFilter/IPRange.php @@ -0,0 +1,106 @@ + 32) { + return false; + } + + $ip = ip2long($ip); + $subnet = ip2long($subnet); + if (false === $ip || false === $subnet) { + // Invalid data + return false; + } + + return 0 === substr_compare( + sprintf("%032b", $ip), + sprintf("%032b", $subnet), + 0, + $mask + ); + } + + /** @psalm-pure */ + public static function matchesIPv6(string $ip, string $cidr): bool + { + $mask = 128; + $subnet = $cidr; + + if (false !== strpos($cidr, '/')) { + [$subnet, $mask] = explode('/', $cidr, 2); + $mask = (int) $mask; + } + + if ($mask < 0 || $mask > 128) { + return false; + } + + $ip = inet_pton($ip); + $subnet = inet_pton($subnet); + + if (false == $ip || false == $subnet) { + // Invalid data + return false; + } + + // mask 0: if it's a valid IP, it's valid + if ($mask === 0) { + return (bool) unpack('n*', $ip); + } + + // @see http://stackoverflow.com/questions/7951061/matching-ipv6-address-to-a-cidr-subnet, MW answer + $binMask = str_repeat("f", intval($mask / 4)); + switch ($mask % 4) { + case 0: + break; + case 1: + $binMask .= "8"; + break; + case 2: + $binMask .= "c"; + break; + case 3: + $binMask .= "e"; + break; + } + + $binMask = str_pad($binMask, 32, '0'); + $binMask = pack("H*", $binMask); + + return ($ip & $binMask) === $subnet; + } +} diff --git a/src/functions/marshal_uri_from_sapi.php b/src/functions/marshal_uri_from_sapi.php index 778d31bf..0092aeff 100644 --- a/src/functions/marshal_uri_from_sapi.php +++ b/src/functions/marshal_uri_from_sapi.php @@ -22,6 +22,8 @@ * * @param array $server SAPI parameters * @param array $headers HTTP request headers + * @deprecated This function is deprecated as of 2.11.1, and will be removed in + * 3.0.0. As of 2.11.1, it is no longer used internally. */ function marshalUriFromSapi(array $server, array $headers) : Uri { diff --git a/test/ServerRequestFactoryTest.php b/test/ServerRequestFactoryTest.php index 085908c2..fe3beb7a 100644 --- a/test/ServerRequestFactoryTest.php +++ b/test/ServerRequestFactoryTest.php @@ -6,11 +6,11 @@ use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequestFactory; +use Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface; use Laminas\Diactoros\UploadedFile; use Laminas\Diactoros\Uri; use PHPUnit\Framework\TestCase; -use ReflectionMethod; -use ReflectionProperty; +use Psr\Http\Message\ServerRequestInterface; use UnexpectedValueException; use function Laminas\Diactoros\marshalHeadersFromSapi; @@ -722,4 +722,34 @@ public function testDoesNotMarshalAllContentPrefixedServerVarsAsHeaders( $this->assertSame($expectedHeaderValue, $request->getHeaderLine($headerName)); $this->assertSame($expectedServerValue, $request->getServerParams()[$key]); } + + public function testReturnsFilteredRequestBasedOnRequestFilterProvided(): void + { + $expectedRequest = new ServerRequest(); + $filter = new class($expectedRequest) implements FilterServerRequestInterface { + /** @var ServerRequestInterface */ + private $request; + + public function __construct(ServerRequestInterface $request) + { + $this->request = $request; + } + + public function __invoke(ServerRequestInterface $request): ServerRequestInterface + { + return $this->request; + } + }; + + $request = ServerRequestFactory::fromGlobals( + ['REMOTE_ADDR' => '127.0.0.1'], + ['foo' => 'bar'], + null, + null, + null, + $filter + ); + + $this->assertSame($expectedRequest, $request); + } } diff --git a/test/ServerRequestFilter/DoNotFilterTest.php b/test/ServerRequestFilter/DoNotFilterTest.php new file mode 100644 index 00000000..bcd60761 --- /dev/null +++ b/test/ServerRequestFilter/DoNotFilterTest.php @@ -0,0 +1,20 @@ +assertSame($request, $filter($request)); + } +} diff --git a/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php b/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php new file mode 100644 index 00000000..653d91d1 --- /dev/null +++ b/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php @@ -0,0 +1,334 @@ + '192.168.1.1'], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = FilterUsingXForwardedHeaders::trustProxies(['192.168.1.0/24']); + + $filteredRequest = $filter($request); + $filteredUri = $filteredRequest->getUri(); + $this->assertNotSame($request->getUri(), $filteredUri); + $this->assertSame('example.com', $filteredUri->getHost()); + $this->assertSame(4433, $filteredUri->getPort()); + $this->assertSame('https', $filteredUri->getScheme()); + } + + public function testTrustingStringProxyWithSpecificTrustedHeadersTrustsOnlyThoseHeadersForTrustedProxy(): void + { + $request = new ServerRequest( + ['REMOTE_ADDR' => '192.168.1.1'], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = FilterUsingXForwardedHeaders::trustProxies( + ['192.168.1.0/24'], + [FilterUsingXForwardedHeaders::HEADER_HOST, FilterUsingXForwardedHeaders::HEADER_PROTO] + ); + + $filteredRequest = $filter($request); + $filteredUri = $filteredRequest->getUri(); + $this->assertNotSame($request->getUri(), $filteredUri); + $this->assertSame('example.com', $filteredUri->getHost()); + $this->assertSame(80, $filteredUri->getPort()); + $this->assertSame('https', $filteredUri->getScheme()); + } + + public function testFilterDoesNothingWhenAddressNotFromTrustedProxy(): void + { + $request = new ServerRequest( + ['REMOTE_ADDR' => '10.0.0.1'], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = FilterUsingXForwardedHeaders::trustProxies(['192.168.1.0/24']); + + $filteredRequest = $filter($request); + $filteredUri = $filteredRequest->getUri(); + $this->assertSame($request->getUri(), $filteredUri); + } + + /** @psalm-return iterable */ + public function trustedProxyList(): iterable + { + yield 'private-class-a-subnet' => ['10.1.1.1']; + yield 'private-class-c-subnet' => ['192.168.1.1']; + } + + /** @dataProvider trustedProxyList */ + public function testTrustingProxyListWithoutExplicitTrustedHeadersTrustsAllForwardedRequestsForTrustedProxies( + string $remoteAddr + ): void { + $request = new ServerRequest( + ['REMOTE_ADDR' => $remoteAddr], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = FilterUsingXForwardedHeaders::trustProxies(['192.168.1.0/24', '10.1.0.0/16']); + + $filteredRequest = $filter($request); + $filteredUri = $filteredRequest->getUri(); + $this->assertNotSame($request->getUri(), $filteredUri); + $this->assertSame('example.com', $filteredUri->getHost()); + $this->assertSame(4433, $filteredUri->getPort()); + $this->assertSame('https', $filteredUri->getScheme()); + } + + /** @dataProvider trustedProxyList */ + public function testTrustingProxyListWithSpecificTrustedHeadersTrustsOnlyThoseHeaders(string $remoteAddr): void + { + $request = new ServerRequest( + ['REMOTE_ADDR' => $remoteAddr], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = FilterUsingXForwardedHeaders::trustProxies( + ['192.168.1.0/24', '10.1.0.0/16'], + [FilterUsingXForwardedHeaders::HEADER_HOST, FilterUsingXForwardedHeaders::HEADER_PROTO] + ); + + $filteredRequest = $filter($request); + $filteredUri = $filteredRequest->getUri(); + $this->assertNotSame($request->getUri(), $filteredUri); + $this->assertSame('example.com', $filteredUri->getHost()); + $this->assertSame(80, $filteredUri->getPort()); + $this->assertSame('https', $filteredUri->getScheme()); + } + + /** @psalm-return iterable */ + public function untrustedProxyList(): iterable + { + yield 'private-class-a-subnet' => ['10.0.0.1']; + yield 'private-class-c-subnet' => ['192.168.168.1']; + } + + /** @dataProvider untrustedProxyList */ + public function testFilterDoesNothingWhenAddressNotInTrustedProxyList(string $remoteAddr): void + { + $request = new ServerRequest( + ['REMOTE_ADDR' => $remoteAddr], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = FilterUsingXForwardedHeaders::trustProxies(['192.168.1.0/24', '10.1.0.0/16']); + + $this->assertSame($request, $filter($request)); + } + + public function testPassingInvalidAddressInProxyListRaisesException(): void + { + $this->expectException(InvalidProxyAddressException::class); + FilterUsingXForwardedHeaders::trustProxies(['192.168.1']); + } + + public function testPassingInvalidForwardedHeaderNamesWhenTrustingProxyRaisesException(): void + { + $this->expectException(InvalidForwardedHeaderNameException::class); + /** + * @psalm-suppress InvalidArgument + */ + FilterUsingXForwardedHeaders::trustProxies(['192.168.1.0/24'], ['Host']); + } + + public function testListOfForwardedHostsIsConsideredUntrusted(): void + { + $request = new ServerRequest( + ['REMOTE_ADDR' => '192.168.1.1'], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com,proxy.api.example.com', + ] + ); + + $filter = FilterUsingXForwardedHeaders::trustAny(); + + $this->assertSame($request, $filter($request)); + } + + public function testListOfForwardedPortsIsConsideredUntrusted(): void + { + $request = new ServerRequest( + ['REMOTE_ADDR' => '192.168.1.1'], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Port' => '8080,9000', + ] + ); + + $filter = FilterUsingXForwardedHeaders::trustAny(); + + $this->assertSame($request, $filter($request)); + } + + public function testListOfForwardedProtosIsConsideredUntrusted(): void + { + $request = new ServerRequest( + ['REMOTE_ADDR' => '192.168.1.1'], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Proto' => 'http,https', + ] + ); + + $filter = FilterUsingXForwardedHeaders::trustAny(); + + $this->assertSame($request, $filter($request)); + } + + /** @psalm-return iterable */ + public function trustedReservedNetworkList(): iterable + { + yield 'ipv4-localhost' => ['127.0.0.1']; + yield 'ipv4-class-a' => ['10.10.10.10']; + yield 'ipv4-class-b' => ['172.16.16.16']; + yield 'ipv4-class-c' => ['192.168.2.1']; + yield 'ipv6-localhost' => ['::1']; + yield 'ipv6-private' => ['fdb4:d239:27bc:1d9f:0001:0001:0001:0001']; + yield 'ipv6-local-link' => ['fe80:0000:0000:0000:abcd:abcd:abcd:abcd']; + } + + /** @dataProvider trustedReservedNetworkList */ + public function testTrustReservedSubnetsProducesFilterThatAcceptsAddressesFromThoseSubnets( + string $remoteAddr + ): void { + $request = new ServerRequest( + ['REMOTE_ADDR' => $remoteAddr], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = FilterUsingXForwardedHeaders::trustReservedSubnets(); + + $filteredRequest = $filter($request); + $filteredUri = $filteredRequest->getUri(); + $this->assertNotSame($request->getUri(), $filteredUri); + $this->assertSame('example.com', $filteredUri->getHost()); + $this->assertSame(4433, $filteredUri->getPort()); + $this->assertSame('https', $filteredUri->getScheme()); + } + + /** @psalm-return iterable */ + public function unreservedNetworkAddressList(): iterable + { + yield 'ipv4-no-localhost' => ['128.0.0.1']; + yield 'ipv4-no-class-a' => ['19.10.10.10']; + yield 'ipv4-not-class-b' => ['173.16.16.16']; + yield 'ipv4-not-class-c' => ['193.168.2.1']; + yield 'ipv6-not-localhost' => ['::2']; + yield 'ipv6-not-private' => ['fab4:d239:27bc:1d9f:0001:0001:0001:0001']; + yield 'ipv6-not-local-link' => ['ef80:0000:0000:0000:abcd:abcd:abcd:abcd']; + } + + /** @dataProvider unreservedNetworkAddressList */ + public function testTrustReservedSubnetsProducesFilterThatRejectsAddressesNotFromThoseSubnets( + string $remoteAddr + ): void { + $request = new ServerRequest( + ['REMOTE_ADDR' => $remoteAddr], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = FilterUsingXForwardedHeaders::trustReservedSubnets(); + + $filteredRequest = $filter($request); + $this->assertSame($request, $filteredRequest); + } +} diff --git a/test/ServerRequestFilter/IPRangeTest.php b/test/ServerRequestFilter/IPRangeTest.php new file mode 100644 index 00000000..65716ebf --- /dev/null +++ b/test/ServerRequestFilter/IPRangeTest.php @@ -0,0 +1,102 @@ + + */ + public function IPv4Data(): array + { + return [ + 'valid - exact (no mask; /32 equiv)' => [true, '192.168.1.1', '192.168.1.1'], + 'valid - entirety of class-c (/1)' => [true, '192.168.1.1', '192.168.1.1/1'], + 'valid - class-c private subnet (/24)' => [true, '192.168.1.1', '192.168.1.0/24'], + 'valid - any subnet (/0)' => [true, '1.2.3.4', '0.0.0.0/0'], + 'valid - subnet expands to all' => [true, '1.2.3.4', '192.168.1.0/0'], + 'invalid - class-a invalid subnet' => [false, '192.168.1.1', '1.2.3.4/1'], + 'invalid - CIDR mask out-of-range' => [false, '192.168.1.1', '192.168.1.1/33'], + 'invalid - invalid cidr notation' => [false, '1.2.3.4', '256.256.256/0'], + 'invalid - invalid IP address' => [false, 'an_invalid_ip', '192.168.1.0/24'], + 'invalid - empty IP address' => [false, '', '1.2.3.4/1'], + 'invalid - proxy wildcard' => [false, '192.168.20.13', '*'], + 'invalid - proxy missing netmask' => [false, '192.168.20.13', '0.0.0.0'], + 'invalid - request IP with invalid proxy wildcard' => [false, '0.0.0.0', '*'], + ]; + } + + /** + * @dataProvider IPv4Data + */ + public function testIPv4(bool $result, string $remoteAddr, string $cidr): void + { + $this->assertSame($result, IPRange::matchesIPv4($remoteAddr, $cidr)); + } + + /** + * @psalm-return array + */ + public function IPv6Data(): array + { + return [ + 'valid - ipv4 subnet' => [true, '2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65'], + 'valid - exact' => [true, '0:0:0:0:0:0:0:1', '::1'], + 'valid - all subnets' => [true, '0:0:603:0:396e:4789:8e99:0001', '::/0'], + 'valid - subnet expands to all' => [true, '0:0:603:0:396e:4789:8e99:0001', '2a01:198:603:0::/0'], + 'invalid - not in subnet' => [false, '2a00:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65'], + 'invalid - does not match exact' => [false, '2a01:198:603:0:396e:4789:8e99:890f', '::1'], + 'invalid - compressed notation, does not match exact' => [false, '0:0:603:0:396e:4789:8e99:0001', '::1'], + 'invalid - garbage IP' => [false, '}__test|O:21:"JDatabaseDriverMysqli":3:{s:2', '::1'], + 'invalid - invalid cidr' => [false, '2a01:198:603:0:396e:4789:8e99:890f', 'unknown'], + 'invalid - empty IP address' => [false, '', '::1'], + ]; + } + + /** + * @dataProvider IPv6Data + */ + public function testIPv6(bool $result, string $remoteAddr, string $cidr): void + { + $this->assertSame($result, IPRange::matchesIPv6($remoteAddr, $cidr)); + } + + /** + * @psalm-return iterable + */ + public function combinedData(): iterable + { + foreach ($this->IPv4Data() as $test => $data) { + $name = "IPv4 - {$test}"; + yield $name => $data; + } + + foreach ($this->IPv6Data() as $test => $data) { + $name = "IPv6 - {$test}"; + yield $name => $data; + } + } + + /** @dataProvider combinedData */ + public function testCombinedIPv4AndIPv6Pool(bool $result, string $remoteAddr, string $cidr): void + { + $this->assertSame($result, IPRange::matches($remoteAddr, $cidr)); + } +}