diff --git a/.editorconfig b/.editorconfig index d152092..c4cb002 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,14 @@ +; top-most EditorConfig file root = true +; Unix-style newlines [*] +end_of_line = LF indent_style = space -end_of_line = lf -charset = utf-8 +indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true -indent_size = 4 +charset = utf-8 + +[.github/**.yml] +indent_size = 2 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..a1738ea --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: j0k3r diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..29acb9a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,73 @@ +name: CI + +on: + pull_request: + branches: + - "master" + push: + branches: + - "master" + +env: + fail-fast: true + +jobs: + unit: + name: PHPUnit (PHP ${{ matrix.php }}) + runs-on: "ubuntu-20.04" + + strategy: + matrix: + php: + - "7.4" + - "8.0" + - "8.1" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + tools: pecl, composer:v2 + extensions: curl + ini-values: "date.timezone=Europe/Paris" + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies with Composer + uses: ramsey/composer-install@v2 + + - name: Run PHPUnit + run: php vendor/bin/simple-phpunit -v + + cs: + name: PHP-CS-Fxier + runs-on: "ubuntu-20.04" + + strategy: + matrix: + php: + - "7.4" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + tools: pecl, composer:v2 + extensions: curl + ini-values: "date.timezone=Europe/Paris" + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies with Composer + uses: ramsey/composer-install@v2 + + - name: Run PHP-CS-Fxier + run: php vendor/bin/php-cs-fixer fix --verbose --dry-run diff --git a/.gitignore b/.gitignore index 8ba6969..25ec21c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ composer.lock build .php_cs.cache .phpunit.result.cache +.php-cs-fixer.cache diff --git a/.php_cs b/.php-cs-fixer.dist.php similarity index 56% rename from .php_cs rename to .php-cs-fixer.dist.php index 683e001..fdf2310 100644 --- a/.php_cs +++ b/.php-cs-fixer.dist.php @@ -1,30 +1,30 @@ in(__DIR__) + ->exclude(['vendor', 'log']) +; + +return (new PhpCsFixer\Config()) ->setRiskyAllowed(true) ->setRules([ '@Symfony' => true, '@Symfony:risky' => true, - // beacuse of PHP 5.3.3 - 'array_syntax' => ['syntax' => 'long'], + 'array_syntax' => ['syntax' => 'short'], 'combine_consecutive_unsets' => true, 'heredoc_to_nowdoc' => true, - 'no_extra_consecutive_blank_lines' => ['break', 'continue', 'extra', 'return', 'throw', 'use', 'parenthesis_brace_block', 'square_brace_block', 'curly_brace_block'], + 'no_extra_blank_lines' => ['tokens' => ['break', 'continue', 'extra', 'return', 'throw', 'use', 'parenthesis_brace_block', 'square_brace_block', 'curly_brace_block']], 'no_unreachable_default_argument_value' => true, 'no_useless_else' => true, 'no_useless_return' => true, 'ordered_class_elements' => true, 'ordered_imports' => true, - 'php_unit_strict' => false, + 'php_unit_strict' => true, 'phpdoc_order' => true, // 'psr4' => true, 'strict_comparison' => true, 'strict_param' => true, 'concat_space' => ['spacing' => 'one'], ]) - ->setFinder( - PhpCsFixer\Finder::create() - ->exclude(['vendor', 'log']) - ->in(__DIR__) - ) + ->setFinder($finder) ; diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index e790b83..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,9 +0,0 @@ -tools: - external_code_coverage: - timeout: 600 - -filter: - excluded_paths: - - 'example/*' - - 'tests/*' - - '*Test.php' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 742a34a..0000000 --- a/.travis.yml +++ /dev/null @@ -1,37 +0,0 @@ -dist: xenial -os: linux -language: php - -php: - - 7.3 - - 7.4 - - nightly - -jobs: - include: - - php: 7.2 - env: CS_FIXER=run - fast_finish: true - allow_failures: - - php: nightly - -# cache vendor dirs -cache: - directories: - - vendor - - $HOME/.composer/cache - -install: - - composer self-update - -before_script: - - composer install --prefer-dist --no-interaction -o - -script: - - php vendor/bin/simple-phpunit -v --coverage-clover=coverage.clover - - if [ "$CS_FIXER" = "run" ]; then php vendor/bin/php-cs-fixer fix --verbose --dry-run ; fi; - -after_script: - - | - wget https://scrutinizer-ci.com/ocular.phar - php ocular.phar code-coverage:upload --format=php-clover coverage.clover diff --git a/README.md b/README.md index 476ffa8..3b355d7 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,12 @@ # SafeCurl -[![Build Status](https://travis-ci.org/j0k3r/safecurl.svg?branch=master)](https://travis-ci.org/j0k3r/safecurl) -[![Code Coverage](https://scrutinizer-ci.com/g/j0k3r/safecurl/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/j0k3r/safecurl/?branch=master) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/j0k3r/safecurl/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/j0k3r/safecurl/?branch=master) +[![CI](https://github.com/j0k3r/safecurl/actions/workflows/tests.yml/badge.svg)](https://github.com/j0k3r/safecurl/actions/workflows/tests.yml) [![Total Downloads](https://poser.pugx.org/j0k3r/safecurl/downloads)](https://packagist.org/packages/j0k3r/safecurl) [![License](https://poser.pugx.org/j0k3r/safecurl/license)](https://packagist.org/packages/j0k3r/safecurl) SafeCurl intends to be a drop-in replacement for the [curl_exec](http://php.net/manual/en/function.curl-exec.php) function in PHP. SafeCurl validates each part of the URL against a white or black list, to help protect against Server-Side Request Forgery attacks. -For more infomation about the project see the blog post ['SafeCurl: SSRF Protection, and a "Capture the Bitcoins"'](http://blog.fin1te.net/post/86235998757/safecurl-ssrf-protection-and-a-capture-the-bitcoins). +For more infomation about the project see the blog post ['SafeCurl: SSRF Protection, and a "Capture the Bitcoins"'](https://whitton.io/articles/safecurl-ssrf-protection-and-a-capture-the-bitcoins/). ## Protections diff --git a/composer.json b/composer.json index 6fbc657..0041894 100644 --- a/composer.json +++ b/composer.json @@ -16,12 +16,12 @@ } ], "require": { - "php": "^7.2|^8.0", + "php": "^7.4|^8.0", "ext-curl": "*" }, "require-dev": { - "symfony/phpunit-bridge": "^5.0", - "friendsofphp/php-cs-fixer": "~2.0" + "symfony/phpunit-bridge": "^6.0", + "friendsofphp/php-cs-fixer": "~3.0" }, "autoload": { "psr-4": { diff --git a/example/default.php b/example/default.php index c32f344..67a636c 100644 --- a/example/default.php +++ b/example/default.php @@ -12,5 +12,5 @@ $safeCurl = new SafeCurl(curl_init()); $result = $safeCurl->execute('https://fin1te.net'); } catch (Exception $e) { - //Handle exception + // Handle exception } diff --git a/example/options.php b/example/options.php index 4f7a732..71aebf6 100644 --- a/example/options.php +++ b/example/options.php @@ -11,15 +11,15 @@ try { $options = new Options(); - //Completely clear the whitelist - $options->setList('whitelist', array()); - //Completely clear the blacklist - $options->setList('blacklist', array()); - //Set the domain whitelist only - $options->setList('whitelist', array('google.com', 'youtube.com'), 'domain'); + // Completely clear the whitelist + $options->setList('whitelist', []); + // Completely clear the blacklist + $options->setList('blacklist', []); + // Set the domain whitelist only + $options->setList('whitelist', ['google.com', 'youtube.com'], 'domain'); $safeCurl = new SafeCurl(curl_init()); $result = $safeCurl->execute('http://www.youtube.com'); } catch (Exception $e) { - //Handle exception + // Handle exception } diff --git a/example/redirects.php b/example/redirects.php index 62b9d03..c47133d 100644 --- a/example/redirects.php +++ b/example/redirects.php @@ -11,11 +11,11 @@ try { $options = new Options(); - //Follow redirects, but limit to 10 + // Follow redirects, but limit to 10 $options->enableFollowLocation()->setFollowLocationLimit(10); $safeCurl = new SafeCurl(curl_init()); $result = $safeCurl->execute('http://fin1te.net'); } catch (Exception $e) { - //Handle exception + // Handle exception } diff --git a/example/url.php b/example/url.php index 03f896d..17393da 100644 --- a/example/url.php +++ b/example/url.php @@ -12,5 +12,5 @@ try { $safeUrl = Url::validateUrl('http://google.com', new Options()); } catch (Exception $e) { - //Handle exception + // Handle exception } diff --git a/src/Options.php b/src/Options.php index 34fdc3d..fc8b134 100644 --- a/src/Options.php +++ b/src/Options.php @@ -29,18 +29,18 @@ class Options /** * @var array */ - private $whitelist = array( - 'ip' => array(), - 'port' => array('80', '443', '8080'), - 'domain' => array(), - 'scheme' => array('http', 'https'), - ); + private $whitelist = [ + 'ip' => [], + 'port' => ['80', '443', '8080'], + 'domain' => [], + 'scheme' => ['http', 'https'], + ]; /** * @var array */ - private $blacklist = array( - 'ip' => array( + private $blacklist = [ + 'ip' => [ '0.0.0.0/8', '10.0.0.0/8', '100.64.0.0/10', @@ -56,11 +56,11 @@ class Options '203.0.113.0/24', '224.0.0.0/4', '240.0.0.0/4', - ), - 'port' => array(), - 'domain' => array(), - 'scheme' => array(), - ); + ], + 'port' => [], + 'domain' => [], + 'scheme' => [], + ]; public function __construct() { @@ -210,7 +210,7 @@ public function disablePinDns() */ public function isInList($list, $type, $value) { - if (!\in_array($list, array('whitelist', 'blacklist'), true)) { + if (!\in_array($list, ['whitelist', 'blacklist'], true)) { throw new InvalidOptionException('Provided list "' . $list . '" must be "whitelist" or "blacklist"'); } @@ -220,15 +220,15 @@ public function isInList($list, $type, $value) if (empty($this->{$list}[$type])) { if ('whitelist' === $list) { - //Whitelist will return true + // Whitelist will return true return true; } - //Blacklist returns false + // Blacklist returns false return false; } - //For domains, a regex match is needed + // For domains, a regex match is needed if ('domain' === $type) { foreach ($this->{$list}[$type] as $domain) { if (preg_match('/^' . $domain . '$/i', $value)) { @@ -252,7 +252,7 @@ public function isInList($list, $type, $value) */ public function getList($list, $type = null) { - if (!\in_array($list, array('whitelist', 'blacklist'), true)) { + if (!\in_array($list, ['whitelist', 'blacklist'], true)) { throw new InvalidOptionException('Provided list "' . $list . '" must be "whitelist" or "blacklist"'); } @@ -278,7 +278,7 @@ public function getList($list, $type = null) */ public function setList($list, $values, $type = null) { - if (!\in_array($list, array('whitelist', 'blacklist'), true)) { + if (!\in_array($list, ['whitelist', 'blacklist'], true)) { throw new InvalidOptionException('Provided list "' . $list . '" must be "whitelist" or "blacklist"'); } @@ -297,7 +297,7 @@ public function setList($list, $values, $type = null) } foreach ($values as $type => $value) { - if (!\in_array($type, array('ip', 'port', 'domain', 'scheme'), true)) { + if (!\in_array($type, ['ip', 'port', 'domain', 'scheme'], true)) { throw new InvalidOptionException('Provided type "' . $type . '" must be "ip", "port", "domain" or "scheme"'); } @@ -318,7 +318,7 @@ public function setList($list, $values, $type = null) */ public function addToList($list, $type, $values) { - if (!\in_array($list, array('whitelist', 'blacklist'), true)) { + if (!\in_array($list, ['whitelist', 'blacklist'], true)) { throw new InvalidOptionException('Provided list "' . $list . '" must be "whitelist" or "blacklist"'); } @@ -330,9 +330,9 @@ public function addToList($list, $type, $values) throw new InvalidOptionException('Provided values cannot be empty'); } - //Cast single values to an array + // Cast single values to an array if (!\is_array($values)) { - $values = array($values); + $values = [$values]; } foreach ($values as $value) { @@ -355,7 +355,7 @@ public function addToList($list, $type, $values) */ public function removeFromList($list, $type, $values) { - if (!\in_array($list, array('whitelist', 'blacklist'), true)) { + if (!\in_array($list, ['whitelist', 'blacklist'], true)) { throw new InvalidOptionException('Provided list "' . $list . '" must be "whitelist" or "blacklist"'); } @@ -367,9 +367,9 @@ public function removeFromList($list, $type, $values) throw new InvalidOptionException('Provided values cannot be empty'); } - //Cast single values to an array + // Cast single values to an array if (!\is_array($values)) { - $values = array($values); + $values = [$values]; } $this->{$list}[$type] = array_diff($this->{$list}[$type], $values); diff --git a/src/SafeCurl.php b/src/SafeCurl.php index 98dae6a..2cfb1d3 100644 --- a/src/SafeCurl.php +++ b/src/SafeCurl.php @@ -53,9 +53,16 @@ public function getCurlHandle() */ public function setCurlHandle($curlHandle) { - if (!\is_resource($curlHandle) || 'curl' !== get_resource_type($curlHandle)) { - //Need a valid cURL resource, throw exception - throw new Exception('SafeCurl expects a valid cURL resource - "' . \gettype($curlHandle) . '" provided.'); + if (version_compare(\PHP_VERSION, '8.0.0', '<')) { + if (!\is_resource($curlHandle) || 'curl' !== get_resource_type($curlHandle)) { + // Need a valid cURL resource, throw exception + throw new Exception('SafeCurl expects a valid cURL resource - "' . \gettype($curlHandle) . '" provided.'); + } + } else { + if (!($curlHandle instanceof \CurlHandle)) { + // Need a valid cURL resource, throw exception + throw new Exception('SafeCurl expects a valid cURL resource - "' . ($curlHandle ? \get_class($curlHandle) : \gettype($curlHandle)) . '" provided.'); + } } $this->curlHandle = $curlHandle; @@ -96,47 +103,47 @@ public function execute($url) $redirectLimit = $this->getOptions()->getFollowLocationLimit(); do { - //Validate the URL + // Validate the URL $url = Url::validateUrl($url, $this->getOptions()); if ($this->getOptions()->getPinDns()) { - //Send a Host header - curl_setopt($this->curlHandle, CURLOPT_HTTPHEADER, array('Host: ' . $url['host'])); - //The "fake" URL - curl_setopt($this->curlHandle, CURLOPT_URL, $url['url']); - //We also have to disable SSL cert verfication, which is not great - //Might be possible to manually check the certificate ourselves? - curl_setopt($this->curlHandle, CURLOPT_SSL_VERIFYPEER, false); + // Send a Host header + curl_setopt($this->curlHandle, \CURLOPT_HTTPHEADER, ['Host: ' . $url['host']]); + // The "fake" URL + curl_setopt($this->curlHandle, \CURLOPT_URL, $url['url']); + // We also have to disable SSL cert verfication, which is not great + // Might be possible to manually check the certificate ourselves? + curl_setopt($this->curlHandle, \CURLOPT_SSL_VERIFYPEER, false); } else { - curl_setopt($this->curlHandle, CURLOPT_URL, $url['url']); + curl_setopt($this->curlHandle, \CURLOPT_URL, $url['url']); } // in case of `CURLINFO_REDIRECT_URL` isn't defined - curl_setopt($this->curlHandle, CURLOPT_HEADER, true); + curl_setopt($this->curlHandle, \CURLOPT_HEADER, true); - //Execute the cURL request + // Execute the cURL request $response = curl_exec($this->curlHandle); - //Check for any errors + // Check for any errors if (curl_errno($this->curlHandle)) { throw new Exception('cURL Error: ' . curl_error($this->curlHandle)); } - //Check for an HTTP redirect + // Check for an HTTP redirect if ($this->getOptions()->getFollowLocation()) { - $statusCode = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE); + $statusCode = curl_getinfo($this->curlHandle, \CURLINFO_HTTP_CODE); switch ($statusCode) { case 301: case 302: case 303: case 307: case 308: - //Redirect received, so rinse and repeat + // Redirect received, so rinse and repeat if (0 === $redirectLimit || ++$redirectCount < $redirectLimit) { // `CURLINFO_REDIRECT_URL` was introduced in 5.3.7 & it doesn't exist in HHVM // use a custom solution is that both case if (\defined('CURLINFO_REDIRECT_URL')) { - $url = curl_getinfo($this->curlHandle, CURLINFO_REDIRECT_URL); + $url = curl_getinfo($this->curlHandle, \CURLINFO_REDIRECT_URL); } else { preg_match('/Location:(.*?)\n/i', $response, $matches); $url = trim(array_pop($matches)); @@ -154,7 +161,7 @@ public function execute($url) } while ($redirected); // since we added header in the response (to retrieve the Location header), don't forget to remove all header - $headerSize = curl_getinfo($this->curlHandle, CURLINFO_HEADER_SIZE); + $headerSize = curl_getinfo($this->curlHandle, \CURLINFO_HEADER_SIZE); $response = substr($response, $headerSize); // substr return false when string goes empty (in case of a HEAD request or when reponse body is empty for example) @@ -170,17 +177,17 @@ public function execute($url) */ protected function init() { - //To start with, disable FOLLOWLOCATION since we'll handle it - curl_setopt($this->curlHandle, CURLOPT_FOLLOWLOCATION, false); + // To start with, disable FOLLOWLOCATION since we'll handle it + curl_setopt($this->curlHandle, \CURLOPT_FOLLOWLOCATION, false); - //Always return the transfer - curl_setopt($this->curlHandle, CURLOPT_RETURNTRANSFER, true); + // Always return the transfer + curl_setopt($this->curlHandle, \CURLOPT_RETURNTRANSFER, true); - //Force IPv4, since this class isn't yet comptible with IPv6 + // Force IPv4, since this class isn't yet comptible with IPv6 $curlVersion = curl_version(); - if ($curlVersion['features'] & CURLOPT_IPRESOLVE) { - curl_setopt($this->curlHandle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + if ($curlVersion['features'] & \CURLOPT_IPRESOLVE) { + curl_setopt($this->curlHandle, \CURLOPT_IPRESOLVE, \CURL_IPRESOLVE_V4); } } } diff --git a/src/Url.php b/src/Url.php index 2abf163..7a19424 100644 --- a/src/Url.php +++ b/src/Url.php @@ -20,11 +20,11 @@ class Url */ public static function validateUrl($url, Options $options) { - if ('' === trim($url)) { + if ('' === trim($url ?? '')) { throw new InvalidURLException('Provided URL "' . $url . '" cannot be empty'); } - //Split URL into parts first + // Split URL into parts first $parts = parse_url($url); if (empty($parts)) { @@ -35,43 +35,43 @@ public static function validateUrl($url, Options $options) throw new InvalidURLException('Provided URL "' . $url . '" doesn\'t contain a hostname'); } - //If credentials are passed in, but we don't want them, raise an exception + // If credentials are passed in, but we don't want them, raise an exception if (!$options->getSendCredentials() && (\array_key_exists('user', $parts) || \array_key_exists('pass', $parts))) { throw new InvalidURLException('Credentials passed in but "sendCredentials" is set to false'); } - //First, validate the scheme + // First, validate the scheme if (\array_key_exists('scheme', $parts)) { $parts['scheme'] = self::validateScheme($parts['scheme'], $options); } else { - //Default to http + // Default to http $parts['scheme'] = 'http'; } - //Validate the port + // Validate the port if (\array_key_exists('port', $parts)) { $parts['port'] = self::validatePort($parts['port'], $options); } - //Validate the host + // Validate the host $host = self::validateHost($parts['host'], $options); if ($options->getPinDns()) { - //Since we're pinning DNS, we replace the host in the URL - //with an IP, then get cURL to send the Host header + // Since we're pinning DNS, we replace the host in the URL + // with an IP, then get cURL to send the Host header $parts['host'] = $host['ips'][0]; } else { - //Not pinning DNS, so just use the host + // Not pinning DNS, so just use the host $parts['host'] = $host['host']; } - //Rebuild the URL + // Rebuild the URL $url = self::buildUrl($parts); - return array( + return [ 'url' => $url, 'host' => $host['host'], 'ips' => $host['ips'], - ); + ]; } /** @@ -86,7 +86,7 @@ public static function validateScheme($scheme, Options $options) { $scheme = strtolower($scheme); - //Whitelist always takes precedence over a blacklist + // Whitelist always takes precedence over a blacklist if (!$options->isInList('whitelist', 'scheme', $scheme)) { throw new InvalidSchemeException('Provided scheme "' . $scheme . '" doesn\'t match whitelisted values: ' . implode(', ', $options->getList('whitelist', 'scheme'))); } @@ -95,7 +95,7 @@ public static function validateScheme($scheme, Options $options) throw new InvalidSchemeException('Provided scheme "' . $scheme . '" matches a blacklisted value'); } - //Existing value is fine + // Existing value is fine return $scheme; } @@ -120,7 +120,7 @@ public static function validatePort($port, Options $options) throw new InvalidPortException('Provided port "' . $port . '" matches a blacklisted value'); } - //Existing value is fine + // Existing value is fine return $port; } @@ -136,7 +136,7 @@ public static function validateHost($host, Options $options) { $host = strtolower($host); - //Check the host against the domain lists + // Check the host against the domain lists if (!$options->isInList('whitelist', 'domain', $host)) { throw new InvalidDomainException('Provided host "' . $host . '" doesn\'t match whitelisted values: ' . implode(', ', $options->getList('whitelist', 'domain'))); } @@ -145,7 +145,7 @@ public static function validateHost($host, Options $options) throw new InvalidDomainException('Provided host "' . $host . '" matches a blacklisted value'); } - //Now resolve to an IP and check against the IP lists + // Now resolve to an IP and check against the IP lists $ips = @gethostbynamel($host); if (empty($ips)) { throw new InvalidDomainException('Provided host "' . $host . '" doesn\'t resolve to an IP address'); @@ -182,7 +182,7 @@ public static function validateHost($host, Options $options) } } - return array('host' => $host, 'ips' => $ips); + return ['host' => $host, 'ips' => $ips]; } /** @@ -199,7 +199,7 @@ public static function buildUrl($parts) $url .= !empty($parts['scheme']) ? $parts['scheme'] . '://' : ''; $url .= !empty($parts['user']) ? $parts['user'] : ''; $url .= !empty($parts['pass']) ? ':' . $parts['pass'] : ''; - //If we have a user or pass, make sure to add an "@" + // If we have a user or pass, make sure to add an "@" $url .= !empty($parts['user']) || !empty($parts['pass']) ? '@' : ''; $url .= !empty($parts['host']) ? $parts['host'] : ''; $url .= !empty($parts['port']) ? ':' . $parts['port'] : ''; @@ -222,7 +222,7 @@ public static function buildUrl($parts) public static function cidrMatch($ip, $cidr) { if (false === strpos($cidr, '/')) { - //It doesn't have a prefix, just a straight IP match + // It doesn't have a prefix, just a straight IP match return $ip === $cidr; } diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php index bafe662..90fe5aa 100644 --- a/tests/OptionsTest.php +++ b/tests/OptionsTest.php @@ -26,20 +26,20 @@ public function testFollowlocation() public function testFollowlocationLimit() { - $this->assertEquals(0, $this->options->getFollowLocationLimit()); + $this->assertSame(0, $this->options->getFollowLocationLimit()); $this->options->setFollowLocationLimit(10); - $this->assertEquals(10, $this->options->getFollowLocationLimit()); + $this->assertSame(10, $this->options->getFollowLocationLimit()); } public function dataForFollowlocationLimit() { - return array( - array(-1), - array('"é"é"é'), - array(null), - ); + return [ + [-1], + ['"é"é"é'], + [null], + ]; } /** @@ -144,26 +144,26 @@ public function testGetListWhitelistWithType() $this->assertCount(1, $list); $this->assertArrayHasKey(0, $list); - $this->assertEquals('0.0.0.0', $list[0]); + $this->assertSame('0.0.0.0', $list[0]); $list = $this->options->getList('whitelist', 'port'); $this->assertCount(3, $list); - $this->assertEquals('80', $list[0]); - $this->assertEquals('443', $list[1]); - $this->assertEquals('8080', $list[2]); + $this->assertSame('80', $list[0]); + $this->assertSame('443', $list[1]); + $this->assertSame('8080', $list[2]); $this->options->addToList('whitelist', 'domain', '(.*)\.fin1te\.net'); $list = $this->options->getList('whitelist', 'domain'); $this->assertCount(1, $list); - $this->assertEquals('(.*)\.fin1te\.net', $list[0]); + $this->assertSame('(.*)\.fin1te\.net', $list[0]); $list = $this->options->getList('whitelist', 'scheme'); $this->assertCount(2, $list); - $this->assertEquals('http', $list[0]); - $this->assertEquals('https', $list[1]); + $this->assertSame('http', $list[0]); + $this->assertSame('https', $list[1]); } public function testGetListBlacklistWithType() @@ -171,25 +171,25 @@ public function testGetListBlacklistWithType() $list = $this->options->getList('blacklist', 'ip'); $this->assertCount(15, $list); - $this->assertEquals('0.0.0.0/8', $list[0]); + $this->assertSame('0.0.0.0/8', $list[0]); $this->options->addToList('blacklist', 'port', '8080'); $list = $this->options->getList('blacklist', 'port'); $this->assertCount(1, $list); - $this->assertEquals('8080', $list[0]); + $this->assertSame('8080', $list[0]); $this->options->addToList('blacklist', 'domain', '(.*)\.fin1te\.net'); $list = $this->options->getList('blacklist', 'domain'); $this->assertCount(1, $list); - $this->assertEquals('(.*)\.fin1te\.net', $list[0]); + $this->assertSame('(.*)\.fin1te\.net', $list[0]); $this->options->addToList('blacklist', 'scheme', 'ftp'); $list = $this->options->getList('blacklist', 'scheme'); $this->assertCount(1, $list); - $this->assertEquals('ftp', $list[0]); + $this->assertSame('ftp', $list[0]); } public function testGetListBadList() @@ -210,13 +210,13 @@ public function testGetListBadType() public function testSetList() { - $this->options->setList('whitelist', array('ip' => array('0.0.0.0'))); + $this->options->setList('whitelist', ['ip' => ['0.0.0.0']]); - $this->assertEquals(array('0.0.0.0'), $this->options->getList('whitelist', 'ip')); + $this->assertSame(['0.0.0.0'], $this->options->getList('whitelist', 'ip')); - $this->options->setList('blacklist', array(22), 'port'); + $this->options->setList('blacklist', [22], 'port'); - $this->assertEquals(array(22), $this->options->getList('blacklist', 'port')); + $this->assertSame([22], $this->options->getList('blacklist', 'port')); } public function testSetListBadList() @@ -224,7 +224,7 @@ public function testSetListBadList() $this->expectException(\fin1te\SafeCurl\Exception\InvalidOptionException::class); $this->expectExceptionMessage('Provided list "noo" must be "whitelist" or "blacklist"'); - $this->options->setList('noo', array()); + $this->options->setList('noo', []); } public function testSetListBadValue() @@ -240,7 +240,7 @@ public function testSetListBadType() $this->expectException(\fin1te\SafeCurl\Exception\InvalidOptionException::class); $this->expectExceptionMessage('Provided type "noo" must be "ip", "port", "domain" or "scheme"'); - $this->options->setList('whitelist', array(), 'noo'); + $this->options->setList('whitelist', [], 'noo'); } public function testSetListBadTypeValue() @@ -248,7 +248,7 @@ public function testSetListBadTypeValue() $this->expectException(\fin1te\SafeCurl\Exception\InvalidOptionException::class); $this->expectExceptionMessage('Provided type "noo" must be "ip", "port", "domain" or "scheme"'); - $this->options->setList('whitelist', array('noo' => 'oops')); + $this->options->setList('whitelist', ['noo' => 'oops']); } public function testAddToListBadList() @@ -306,7 +306,7 @@ public function testRemoveFromList() $list = $this->options->getList('blacklist', 'port'); $this->assertCount(1, $list); - $this->assertEquals('8080', $list[0]); + $this->assertSame('8080', $list[0]); $this->options->removeFromList('blacklist', 'port', '8080'); $list = $this->options->getList('blacklist', 'port'); @@ -318,9 +318,9 @@ public function testRemoveFromList() $list = $this->options->getList('blacklist', 'scheme'); $this->assertCount(1, $list); - $this->assertEquals('ftp', $list[0]); + $this->assertSame('ftp', $list[0]); - $this->options->removeFromList('blacklist', 'scheme', array('ftp')); + $this->options->removeFromList('blacklist', 'scheme', ['ftp']); $list = $this->options->getList('blacklist', 'scheme'); $this->assertCount(0, $list); diff --git a/tests/SafeCurlTest.php b/tests/SafeCurlTest.php index 9436f2b..cea51f9 100644 --- a/tests/SafeCurlTest.php +++ b/tests/SafeCurlTest.php @@ -13,7 +13,7 @@ public function testFunctionnalGET() $response = $safeCurl->execute('http://www.google.com'); $this->assertNotEmpty($response); - $this->assertEquals($handle, $safeCurl->getCurlHandle()); + $this->assertSame($handle, $safeCurl->getCurlHandle()); $this->assertStringNotContainsString('HTTP/1.1 302 Found', $response); } @@ -22,13 +22,13 @@ public function testFunctionnalHEAD() $handle = curl_init(); // for an unknown reason, HEAD request failed: https://travis-ci.org/j0k3r/safecurl/jobs/91936743 // curl_setopt($handle, CURLOPT_CUSTOMREQUEST, 'HEAD'); - curl_setopt($handle, CURLOPT_NOBODY, true); + curl_setopt($handle, \CURLOPT_NOBODY, true); $safeCurl = new SafeCurl($handle); $response = $safeCurl->execute('http://40.media.tumblr.com/39e917383bf5fe228b82fef850251220/tumblr_nxyw8cjiYx1u7jfjwo1_100.jpg'); - $this->assertEquals('', $response); - $this->assertEquals($handle, $safeCurl->getCurlHandle()); + $this->assertSame('', $response); + $this->assertSame($handle, $safeCurl->getCurlHandle()); $this->assertStringNotContainsString('HTTP/1.1 302 Found', $response); } @@ -42,53 +42,53 @@ public function testBadCurlHandler() public function dataForBlockedUrl() { - return array( - array( + return [ + [ 'http://0.0.0.0:123', \fin1te\SafeCurl\Exception\InvalidURLException\InvalidPortException::class, 'Provided port "123" doesn\'t match whitelisted values: 80, 443, 8080', - ), - array( + ], + [ 'http://127.0.0.1/server-status', \fin1te\SafeCurl\Exception\InvalidURLException\InvalidIPException::class, 'Provided host "127.0.0.1" resolves to "127.0.0.1", which matches a blacklisted value: 127.0.0.0/8', - ), - array( + ], + [ 'file:///etc/passwd', \fin1te\SafeCurl\Exception\InvalidURLException::class, 'Provided URL "file:///etc/passwd" doesn\'t contain a hostname', - ), - array( + ], + [ 'ssh://localhost', \fin1te\SafeCurl\Exception\InvalidURLException\InvalidSchemeException::class, 'Provided scheme "ssh" doesn\'t match whitelisted values: http, https', - ), - array( + ], + [ 'gopher://localhost', \fin1te\SafeCurl\Exception\InvalidURLException\InvalidSchemeException::class, 'Provided scheme "gopher" doesn\'t match whitelisted values: http, https', - ), - array( + ], + [ 'telnet://localhost:25', \fin1te\SafeCurl\Exception\InvalidURLException\InvalidSchemeException::class, 'Provided scheme "telnet" doesn\'t match whitelisted values: http, https', - ), - array( + ], + [ 'http://169.254.169.254/latest/meta-data/', \fin1te\SafeCurl\Exception\InvalidURLException\InvalidIPException::class, 'Provided host "169.254.169.254" resolves to "169.254.169.254", which matches a blacklisted value: 169.254.0.0/16', - ), - array( + ], + [ 'ftp://myhost.com', \fin1te\SafeCurl\Exception\InvalidURLException\InvalidSchemeException::class, 'Provided scheme "ftp" doesn\'t match whitelisted values: http, https', - ), - array( + ], + [ 'http://user:pass@safecurl.fin1te.net?@google.com/', \fin1te\SafeCurl\Exception\InvalidURLException::class, 'Credentials passed in but "sendCredentials" is set to false', - ), - ); + ], + ]; } /** @@ -105,18 +105,18 @@ public function testBlockedUrl($url, $exception, $message) public function dataForBlockedUrlByOptions() { - return array( - array( + return [ + [ 'http://login:password@google.fr', \fin1te\SafeCurl\Exception\InvalidURLException::class, 'Credentials passed in but "sendCredentials" is set to false', - ), - array( + ], + [ 'http://safecurl.fin1te.net', \fin1te\SafeCurl\Exception\InvalidURLException::class, 'Provided host "safecurl.fin1te.net" matches a blacklisted value', - ), - ); + ], + ]; } /** @@ -189,7 +189,7 @@ public function testWithCurlTimeout() $this->expectExceptionMessage('cURL Error:'); $handle = curl_init(); - curl_setopt($handle, CURLOPT_TIMEOUT_MS, 1); + curl_setopt($handle, \CURLOPT_TIMEOUT_MS, 1); $safeCurl = new SafeCurl($handle); $safeCurl->execute('https://httpstat.us/200?sleep=100'); diff --git a/tests/UrlTest.php b/tests/UrlTest.php index 01a9d5b..acec755 100644 --- a/tests/UrlTest.php +++ b/tests/UrlTest.php @@ -7,48 +7,48 @@ class UrlTest extends \PHPUnit\Framework\TestCase { public function dataForValidate() { - return array( - array( + return [ + [ null, \fin1te\SafeCurl\Exception\InvalidURLException::class, 'Provided URL "" cannot be empty', - ), - array( + ], + [ 'http://user@:80', \fin1te\SafeCurl\Exception\InvalidURLException::class, 'Error parsing URL "http://user@:80"', - ), - array( + ], + [ 'http:///example.com/', \fin1te\SafeCurl\Exception\InvalidURLException::class, 'Error parsing URL "http:///example.com/"', - ), - array( + ], + [ 'http://:80', \fin1te\SafeCurl\Exception\InvalidURLException::class, 'Error parsing URL "http://:80"', - ), - array( + ], + [ '/nohost', \fin1te\SafeCurl\Exception\InvalidURLException::class, 'Provided URL "/nohost" doesn\'t contain a hostname', - ), - array( + ], + [ 'ftp://domain.io', \fin1te\SafeCurl\Exception\InvalidURLException\InvalidSchemeException::class, 'Provided scheme "ftp" doesn\'t match whitelisted values: http, https', - ), - array( + ], + [ 'http://domain.io:22', \fin1te\SafeCurl\Exception\InvalidURLException\InvalidPortException::class, 'Provided port "22" doesn\'t match whitelisted values: 80, 443, 8080', - ), - array( + ], + [ 'http://login:password@google.fr:80', \fin1te\SafeCurl\Exception\InvalidURLException::class, 'Credentials passed in but "sendCredentials" is set to false', - ), - ); + ], + ]; } /** @@ -164,8 +164,10 @@ public function testValidateUrlOk() $this->assertArrayHasKey('host', $res); $this->assertArrayHasKey('ips', $res); $this->assertArrayHasKey(0, $res['ips']); - $this->assertEquals('http://104.24.104.11:8080', $res['url']); - $this->assertEquals('www.fin1te.net', $res['host']); + $this->assertStringStartsWith('http://', $res['url']); + $this->assertStringEndsWith(':8080', $res['url']); + $this->assertTrue(false !== filter_var(str_replace(['http://', ':8080'], '', $res['url']), \FILTER_VALIDATE_IP), $res['url'] . ' does not contain a valid IP'); + $this->assertSame('www.fin1te.net', $res['host']); $res = Url::validateUrl('http://www.fin1te.net:8080', new Options()); @@ -174,7 +176,7 @@ public function testValidateUrlOk() $this->assertArrayHasKey('host', $res); $this->assertArrayHasKey('ips', $res); $this->assertArrayHasKey(0, $res['ips']); - $this->assertEquals('http://www.fin1te.net:8080', $res['url']); - $this->assertEquals('www.fin1te.net', $res['host']); + $this->assertSame('http://www.fin1te.net:8080', $res['url']); + $this->assertSame('www.fin1te.net', $res['host']); } }