diff --git a/.gitignore b/.gitignore index 073e37a..b6bd0e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build composer.lock vendor +.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml index be9e9b7..68b8d92 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,9 @@ dist: trusty language: php php: - - 7.0 - - 7.1 - 7.2 - 7.3 - - hhvm + - 7.4 # This triggers builds to run on the new TravisCI infrastructure. # See: http://docs.travis-ci.com/user/workers/container-based-infrastructure/ @@ -17,23 +15,9 @@ cache: directories: - $HOME/.composer/cache -matrix: - include: - - php: 7.0 - env: 'COMPOSER_FLAGS="--prefer-stable --prefer-lowest"' - allow_failures: - - php: hhvm - before_script: - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-dist script: - vendor/bin/phpcs --standard=psr2 src/ - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover - -after_script: - - | - if [[ "$TRAVIS_PHP_VERSION" != 'hhvm' && "$TRAVIS_PHP_VERSION" = '7.0' ]]; then - wget https://scrutinizer-ci.com/ocular.phar - php ocular.phar code-coverage:upload --format=php-clover coverage.clover - fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..35cf6ee --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# CHANGELOG + +### 2.0.0 (November 2020) + +- A `guzzle_opts` option is supported in the settings, which allows full Guzzle + option overrides. +- A `timeout` option is supported in the settings, which is the request timeout + value, and defaults to 2 seconds. + **BREAKING**: this was previously unconfigurable and was 0 seconds, + i.e. infinite timeout. +- The `buildHeaders` method on the main `IPinfo` client is now private. + **BREAKING**: this will no longer be available for use from the client. +- Only non-EOL PHP 7 versions are supported. In particular, PHP 7.2 and above + are all supported and tested in the CI. diff --git a/README.md b/README.md index dbbe046..14ffebc 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,16 @@ It's possible to use a custom cache by creating a child class of the [CacheInter >>> $client = new IPinfo($access_token, $settings); ``` +### Overriding HTTP Client options + +The IPinfo client constructor accepts a `timeout` key which is the request +timeout in seconds. + +For full flexibility, a `guzzle_opts` key is accepted which accepts an +associative array which is described in [Guzzle Request Options](https://docs.guzzlephp.org/en/stable/request-options.html). +Options set here will override any custom settings set by the IPinfo client +internally in case of conflict, including headers. + ### Internationalization When looking up an IP address, the response object includes a `Details->country_name` attribute which includes the country name based on American English. It is possible to return the country name in other languages by setting the `countries_file` keyword argument when creating the `IPinfo` object. diff --git a/composer.json b/composer.json index 4b50123..314e40a 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,12 @@ "email": "jameshtimmins@gmail.com", "homepage": "https://github.com/jhtimmins", "role": "Developer" + }, + { + "name": "Uman Shahzad", + "email": "uman@mslm.io", + "homepage": "https://github.com/UmanShahzad", + "role": "Developer" } ], "require": { @@ -42,7 +48,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "config": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a06c3d8..23e66c8 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,26 +1,16 @@ - - - - tests - - - - - src/ - - - - - - + + + + src/ + + + + + tests + + + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..d234691 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/src/IPinfo.php b/src/IPinfo.php index 92aeac8..89b0e07 100644 --- a/src/IPinfo.php +++ b/src/IPinfo.php @@ -18,6 +18,7 @@ class IPinfo const COUNTRIES_FILE_DEFAULT = __DIR__ . '/countries.json'; const REQUEST_TYPE_GET = 'GET'; const STATUS_CODE_QUOTA_EXCEEDED = 429; + const REQUEST_TIMEOUT_DEFAULT = 2; // seconds public $access_token; public $cache; @@ -27,7 +28,20 @@ class IPinfo public function __construct($access_token = null, $settings = []) { $this->access_token = $access_token; - $this->http_client = new Client(['http_errors' => false]); + + /* + Support a timeout first-class, then a `guzzle_opts` key that can + override anything. + */ + $guzzle_opts = [ + 'http_errors' => false, + 'headers' => $this->buildHeaders(), + 'timeout' => $settings['timeout'] ?? self::REQUEST_TIMEOUT_DEFAULT + ]; + if (isset($settings['guzzle_opts'])) { + $guzzle_opts = array_merge($guzzle_opts, $settings['guzzle_opts']); + } + $this->http_client = new Client($guzzle_opts); $countries_file = $settings['countries_file'] ?? self::COUNTRIES_FILE_DEFAULT; $this->countries = $this->readCountryNames($countries_file); @@ -50,7 +64,6 @@ public function __construct($access_token = null, $settings = []) public function getDetails($ip_address = null) { $response_details = $this->getRequestDetails((string) $ip_address); - return $this->formatDetailsObject($response_details); } @@ -84,56 +97,57 @@ public function formatDetailsObject($details = []) */ public function getRequestDetails(string $ip_address) { - if (!$this->cache->has($ip_address)) { - $url = self::API_URL; - if ($ip_address) { - $url .= "/$ip_address"; - } - - try { - $response = $this->http_client->request( - self::REQUEST_TYPE_GET, - $url, - $this->buildHeaders() - ); - } catch (GuzzleException $e) { - throw new IPinfoException($e->getMessage()); - } catch (Exception $e) { - throw new IPinfoException($e->getMessage()); - } - - if ($response->getStatusCode() == self::STATUS_CODE_QUOTA_EXCEEDED) { - throw new IPinfoException('IPinfo request quota exceeded.'); - } elseif ($response->getStatusCode() >= 400) { - throw new IPinfoException('Exception: ' . json_encode([ + if ($this->cache->has($ip_address)) { + return $this->cache->get($ip_address); + } + + $url = self::API_URL; + if ($ip_address) { + $url .= "/$ip_address"; + } + + try { + $response = $this->http_client->request( + self::REQUEST_TYPE_GET, + $url + ); + } catch (GuzzleException $e) { + throw new IPinfoException($e->getMessage()); + } catch (Exception $e) { + throw new IPinfoException($e->getMessage()); + } + + if ($response->getStatusCode() == self::STATUS_CODE_QUOTA_EXCEEDED) { + throw new IPinfoException('IPinfo request quota exceeded.'); + } elseif ($response->getStatusCode() >= 400) { + throw new IPinfoException('Exception: ' . json_encode([ 'status' => $response->getStatusCode(), 'reason' => $response->getReasonPhrase(), - ])); - } - - $raw_details = json_decode($response->getBody(), true); - $this->cache->set($ip_address, $raw_details); + ])); } - return $this->cache->get($ip_address); + $raw_details = json_decode($response->getBody(), true); + $this->cache->set($ip_address, $raw_details); + + return $raw_details; } /** * Build headers for API request. * @return array Headers for API request. */ - public function buildHeaders() + private function buildHeaders() { $headers = [ - 'user-agent' => 'IPinfoClient/PHP/1.0', - 'accept' => 'application/json', + 'user-agent' => 'IPinfoClient/PHP/2.0', + 'accept' => 'application/json', ]; if ($this->access_token) { $headers['authorization'] = "Bearer {$this->access_token}"; } - return ['headers' => $headers]; + return $headers; } /** diff --git a/tests/DefaultCacheTest.php b/tests/DefaultCacheTest.php index 6a1f3d2..1e45563 100644 --- a/tests/DefaultCacheTest.php +++ b/tests/DefaultCacheTest.php @@ -7,78 +7,78 @@ class DefaultCacheTest extends TestCase { - public function testHasValue() - { - $cache = new DefaultCache($maxsize=4, $ttl=2); - $key1 = 'test'; - $value1 = 'obama'; - $cache->set($key1, $value1); - - $key2 = 'test2'; - $value2 = 'mccain'; - $cache->set($key2, $value2); - - $this->assertTrue($cache->has($key1)); - $this->assertTrue($cache->has($key2)); - } - - public function testDoesNotHaveValue() - { - $cache = new DefaultCache($maxsize=4, $ttl=2); - $key = 'test'; - - $this->assertFalse($cache->has($key)); - } - - public function testGetValue() - { - $cache = new DefaultCache($maxsize=4, $ttl=2); - $key1 = 'test'; - $value1 = 'obama'; - $cache->set($key1, $value1); - - $key2 = 'test2'; - $value2 = 'mccain'; - $cache->set($key2, $value2); - - $this->assertEquals($value1, $cache->get($key1)); - $this->assertEquals($value2, $cache->get($key2)); - } - - public function testMaxSizeExceeded() - { - $cache = new DefaultCache($maxsize=2, $ttl=2); - - $key1 = 'test'; - $value1 = 'obama'; - $cache->set($key1, $value1); - $this->assertEquals($value1, $cache->get($key1)); - - $key2 = 'test2'; - $value2 = 'mccain'; - $cache->set($key2, $value2); - $this->assertEquals($value2, $cache->get($key2)); - - // Test that once the maxsize is exceeded, the earliest item is pushed out. - $key3 = 'test3'; - $value3 = 'gore'; - $cache->set($key3, $value3); - $this->assertEquals(null, $cache->get($key1)); - $this->assertEquals($value2, $cache->get($key2)); - $this->assertEquals($value3, $cache->get($key3)); - } - - public function testTimeToLiveExceeded() - { - $cache = new DefaultCache($maxsize=2, $ttl=1); - - $key = 'test'; - $value = 'obama'; - $cache->set($key, $value); - $this->assertEquals($value, $cache->get($key)); - - // Let the TTL expire. - sleep(2); - $this->assertEquals(null, $cache->get($key)); - } + public function testHasValue() + { + $cache = new DefaultCache($maxsize = 4, $ttl = 2); + $key1 = 'test'; + $value1 = 'obama'; + $cache->set($key1, $value1); + + $key2 = 'test2'; + $value2 = 'mccain'; + $cache->set($key2, $value2); + + $this->assertTrue($cache->has($key1)); + $this->assertTrue($cache->has($key2)); + } + + public function testDoesNotHaveValue() + { + $cache = new DefaultCache($maxsize = 4, $ttl = 2); + $key = 'test'; + + $this->assertFalse($cache->has($key)); + } + + public function testGetValue() + { + $cache = new DefaultCache($maxsize = 4, $ttl = 2); + $key1 = 'test'; + $value1 = 'obama'; + $cache->set($key1, $value1); + + $key2 = 'test2'; + $value2 = 'mccain'; + $cache->set($key2, $value2); + + $this->assertEquals($value1, $cache->get($key1)); + $this->assertEquals($value2, $cache->get($key2)); + } + + public function testMaxSizeExceeded() + { + $cache = new DefaultCache($maxsize = 2, $ttl = 2); + + $key1 = 'test'; + $value1 = 'obama'; + $cache->set($key1, $value1); + $this->assertEquals($value1, $cache->get($key1)); + + $key2 = 'test2'; + $value2 = 'mccain'; + $cache->set($key2, $value2); + $this->assertEquals($value2, $cache->get($key2)); + + // Test that once the maxsize is exceeded, the earliest item is pushed out. + $key3 = 'test3'; + $value3 = 'gore'; + $cache->set($key3, $value3); + $this->assertEquals(null, $cache->get($key1)); + $this->assertEquals($value2, $cache->get($key2)); + $this->assertEquals($value3, $cache->get($key3)); + } + + public function testTimeToLiveExceeded() + { + $cache = new DefaultCache($maxsize = 2, $ttl = 1); + + $key = 'test'; + $value = 'obama'; + $cache->set($key, $value); + $this->assertEquals($value, $cache->get($key)); + + // Let the TTL expire. + sleep(2); + $this->assertEquals(null, $cache->get($key)); + } } diff --git a/tests/DetailsTest.php b/tests/DetailsTest.php index 12be5c0..1792fb0 100644 --- a/tests/DetailsTest.php +++ b/tests/DetailsTest.php @@ -17,20 +17,20 @@ public function testLookupAll() public function testLookupSpecificExists() { - $raw_details = ['country' => 'United States', 'country_code' => 'US']; - $details = new Details($raw_details); + $raw_details = ['country' => 'United States', 'country_code' => 'US']; + $details = new Details($raw_details); - $this->assertTrue(property_exists($details, 'country')); - $this->assertTrue(property_exists($details, 'country_code')); - $this->assertSame($raw_details['country'], $details->country); - $this->assertSame($raw_details['country_code'], $details->country_code); + $this->assertTrue(property_exists($details, 'country')); + $this->assertTrue(property_exists($details, 'country_code')); + $this->assertSame($raw_details['country'], $details->country); + $this->assertSame($raw_details['country_code'], $details->country_code); } public function testLookupSpecificDoesNotExist() { - $raw_details = []; - $details = new Details($raw_details); + $raw_details = []; + $details = new Details($raw_details); - $this->assertFalse(property_exists($details, 'country')); + $this->assertFalse(property_exists($details, 'country')); } } diff --git a/tests/IPinfoTest.php b/tests/IPinfoTest.php index b93e63d..007cdc7 100644 --- a/tests/IPinfoTest.php +++ b/tests/IPinfoTest.php @@ -10,78 +10,128 @@ class IPinfoTest extends TestCase { public function testAccessToken() { - $access_token = '123'; - $client = new IPinfo($access_token); - $this->assertSame($access_token, $client->access_token); + $tok = '123'; + $client = new IPinfo($tok); + $this->assertSame($tok, $client->access_token); } public function testDefaultCountries() { - $client = new IPinfo(); - $this->assertSame('United States', $client->countries['US']); - $this->assertSame('France', $client->countries['FR']); + $client = new IPinfo(); + $this->assertSame('United States', $client->countries['US']); + $this->assertSame('France', $client->countries['FR']); } public function testCustomCache() { - $access_token = 'this is a fake access token'; - $cache = 'this is a fake cache'; - $client = new IPinfo($access_token, ['cache' => $cache]); - $this->assertSame($cache, $client->cache); + $tok = 'this is a fake access token'; + $cache = 'this is a fake cache'; + $client = new IPinfo($tok, ['cache' => $cache]); + $this->assertSame($cache, $client->cache); } public function testDefaultCacheSettings() { - $client = new IPinfo(); - $this->assertSame(IPinfo::CACHE_MAXSIZE, $client->cache->maxsize); - $this->assertSame(IPinfo::CACHE_TTL, $client->cache->ttl); + $client = new IPinfo(); + $this->assertSame(IPinfo::CACHE_MAXSIZE, $client->cache->maxsize); + $this->assertSame(IPinfo::CACHE_TTL, $client->cache->ttl); } public function testCustomCacheSettings() { - $access_token = 'this is a fake access token'; - $settings = ['cache_maxsize' => 100, 'cache_ttl' => 11]; - $client = new IPinfo($access_token, $settings); - $this->assertSame($settings['cache_maxsize'], $client->cache->maxsize); - $this->assertSame($settings['cache_ttl'], $client->cache->ttl); + $tok = 'this is a fake access token'; + $settings = ['cache_maxsize' => 100, 'cache_ttl' => 11]; + $client = new IPinfo($tok, $settings); + $this->assertSame($settings['cache_maxsize'], $client->cache->maxsize); + $this->assertSame($settings['cache_ttl'], $client->cache->ttl); } public function testFormatDetailsObject() { - $test_details = [ - 'country' => 'US', - 'loc' => '123,567' - ]; - - $handler = new IPinfo(); - $details = $handler->formatDetailsObject($test_details); - - $this->assertEquals($test_details['country'], $details->country); - $this->assertEquals('United States', $details->country_name); - $this->assertEquals($test_details['loc'], $details->loc); - $this->assertEquals('123', $details->latitude); - $this->assertEquals('567', $details->longitude); + $test_details = [ + 'country' => 'US', + 'loc' => '123,567' + ]; + + $h = new IPinfo(); + $res = $h->formatDetailsObject($test_details); + + $this->assertEquals($test_details['country'], $res->country); + $this->assertEquals('United States', $res->country_name); + $this->assertEquals($test_details['loc'], $res->loc); + $this->assertEquals('123', $res->latitude); + $this->assertEquals('567', $res->longitude); } - public function testBuildHeaders() + public function testBadIP() { - $token = '123abc'; + $ip = "fake_ip"; + $h = new IPinfo(); + $this->expectException(IPinfoException::class); + $h->getDetails($ip); + } - $handler = new IPinfo($token); - $headers = $handler->buildHeaders(); + public function testLookup() + { + $tok = getenv('IPINFO_TOKEN'); + if (!$tok) { + $this->markTestSkipped('IPINFO_TOKEN env var required'); + } - $this->assertArrayHasKey('headers', $headers); - $headers = $headers['headers']; - $this->assertEquals("IPinfoClient/PHP/1.0", $headers['user-agent']); - $this->assertEquals("application/json", $headers['accept']); - $this->assertEquals("Bearer $token", $headers['authorization']); + $h = new IPinfo($tok); + $ip = "8.8.8.8"; + + /* test multiple times for cache hits */ + for ($i = 0; $i < 5; $i++) { + $res = $h->getDetails($ip); + $this->assertEquals($res->ip, '8.8.8.8'); + $this->assertEquals($res->hostname, 'dns.google'); + $this->assertEquals($res->city, 'Mountain View'); + $this->assertEquals($res->region, 'California'); + $this->assertEquals($res->country, 'US'); + $this->assertEquals($res->country_name, 'United States'); + $this->assertEquals($res->loc, '37.4056,-122.0775'); + $this->assertEquals($res->latitude, '37.4056'); + $this->assertEquals($res->longitude, '-122.0775'); + $this->assertEquals($res->postal, '94043'); + $this->assertEquals($res->timezone, 'America/Los_Angeles'); + $this->assertEquals($res->asn['asn'], 'AS15169'); + $this->assertEquals($res->asn['name'], 'Google LLC'); + $this->assertEquals($res->asn['domain'], 'google.com'); + $this->assertEquals($res->asn['route'], '8.8.8.0/24'); + $this->assertEquals($res->asn['type'], 'business'); + $this->assertEquals($res->company['name'], 'Google LLC'); + $this->assertEquals($res->company['domain'], 'google.com'); + $this->assertEquals($res->company['type'], 'business'); + $this->assertEquals($res->privacy['vpn'], false); + $this->assertEquals($res->privacy['proxy'], false); + $this->assertEquals($res->privacy['tor'], false); + $this->assertEquals($res->privacy['hosting'], false); + $this->assertEquals($res->abuse['address'], 'US, CA, Mountain View, 1600 Amphitheatre Parkway, 94043'); + $this->assertEquals($res->abuse['country'], 'US'); + $this->assertEquals($res->abuse['email'], 'network-abuse@google.com'); + $this->assertEquals($res->abuse['name'], 'Abuse'); + $this->assertEquals($res->abuse['network'], '8.8.8.0/24'); + $this->assertEquals($res->abuse['phone'], '+1-650-253-0000'); + $this->assertEquals($res->domains['ip'], '8.8.8.8'); + } } - public function testBadIP() + public function testGuzzleOverride() { - $ip = "fake_ip"; - $handler = new IPinfo(); - $this->expectException(IPinfoException::class); - $handler->getDetails($ip); + $tok = getenv('IPINFO_TOKEN'); + if (!$tok) { + $this->markTestSkipped('IPINFO_TOKEN env var required'); + } + + $h = new IPinfo($tok, ['guzzle_opts' => [ + 'headers' => [ + 'authorization' => 'Bearer blah' + ], + ]]); + $ip = "8.8.8.8"; + + $this->expectException(IPinfoException::class); + $res = $h->getDetails($ip); } }