diff --git a/src/cache/DefaultCache.php b/src/cache/DefaultCache.php index fe89d49..28f1c79 100644 --- a/src/cache/DefaultCache.php +++ b/src/cache/DefaultCache.php @@ -3,6 +3,7 @@ namespace ipinfo\ipinfo\cache; use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\CacheItem; use Symfony\Contracts\Cache\ItemInterface; /** @@ -32,6 +33,8 @@ public function __construct(int $maxsize, int $ttl) */ public function has(string $name): bool { + $name = $this->sanitizeName($name); + return $this->cache->hasItem($name); } @@ -42,11 +45,12 @@ public function has(string $name): bool */ public function set(string $name, $value) { + $name = $this->sanitizeName($name); if (!$this->cache->hasItem($name)) { $this->element_queue[] = $name; } - $this->cache->get($name, function (ItemInterface $item) use ($value) { + $this->cache->get($name, function (ItemInterface $item) use ($value) { $item->set($value)->expiresAfter($this->ttl); return $item->get(); }); @@ -59,9 +63,20 @@ public function set(string $name, $value) * @param string $ip_address IP address to lookup in cache. * @return mixed IP address data. */ - public function get(string $name) + public function get(string $ip_address) { - return $this->cache->getItem($name)->get(); + $sanitizeName = $this->sanitizeName($ip_address); + $result = $this->cache->getItem($sanitizeName)->get(); + if (is_array($result) && array_key_exists("ip", $result)) { + /** + * The IPv6 may have different notation and we don't know which one is cached. + * We want to give the user the same notation as the one used in his request which may be different from + * the one used in the cache. + */ + $result["ip"] = $this->getIpAddress($ip_address); + } + + return $result; } /** @@ -79,4 +94,34 @@ private function manageSize() $this->element_queue = array_slice($this->element_queue, $overflow); } } + + private function getIpAddress(string $name): string + { + // The $name has the version postfix applied, we need to extract the IP address without it + $parts = explode('_', $name); + return $parts[0]; + } + + /** + * Remove forbidden characters from cache keys + */ + private function sanitizeName(string $name): string + { + // The $name has the version postfix applied, we need to extract the IP address without it + $parts = explode('_', $name); + $ip = $parts[0]; + try { + // Attempt to normalize the IPv6 address + $binary = @inet_pton($ip); // Convert to 16-byte binary + if ($binary !== false && strlen($binary) === 16) { // Valid IPv6 + $ip = inet_ntop($binary); // Convert to full notation (e.g., 2001:0db8:...) + } + $name = $ip . '_' . implode('_', array_slice($parts, 1)); + } catch (\Exception) { + // If invalid, proceed with original input + } + + $forbiddenCharacters = str_split(CacheItem::RESERVED_CHARACTERS); + return str_replace($forbiddenCharacters, '^', $name); + } } diff --git a/tests/DefaultCacheTest.php b/tests/DefaultCacheTest.php index 1e45563..3a895b6 100644 --- a/tests/DefaultCacheTest.php +++ b/tests/DefaultCacheTest.php @@ -81,4 +81,136 @@ public function testTimeToLiveExceeded() sleep(2); $this->assertEquals(null, $cache->get($key)); } + + public function testCacheWithIPv6DifferentNotations() + { + // Create cache instance + $cache = new DefaultCache(10, 600); + + // Original IPv6 address + $standard_ip = "2607:f8b0:4005:805::200e"; + $standard_value = "standard_value"; + $cache->set($standard_ip, $standard_value); + + // Variations with zeros + $variations = [ + "2607:f8b0:4005:805:0:0:0:200e", // Full form + "2607:f8b0:4005:805:0000:0000:0000:200e", // Full form with leading zeros + "2607:f8b0:4005:805:0000:00:00:200e", // Full form with few leading zeros + "2607:F8B0:4005:805::200E", // Uppercase notation + "2607:f8b0:4005:0805::200e", // Leading zero in a group + "2607:f8b0:4005:805:0::200e", // Partially expanded + "2607:f8b0:4005:805:0000::200e", // Full zeros in a second group + ]; + + // DefaultCache does normalize IPs, so we need to check if the cache has the same value + foreach ($variations as $ip) { + $this->assertTrue($cache->has($ip), "Cache should have variation: $ip"); + } + } + + public function testDefaultCacheWithIPv4AndPostfixes() + { + // Create cache + $cache = new DefaultCache(5, 600); + + // Test IPv4 with various postfixes + $ipv4 = '8.8.8.8'; + $postfixes = ['_v1', '_latest', '_beta', '_test_123']; + + foreach ($postfixes as $postfix) { + $key = $ipv4 . $postfix; + $value = "value_for_$key"; + + // Set value with postfix + $cache->set($key, $value); + + // Verify it's in cache + $this->assertTrue($cache->has($key), "Cache should have key with postfix: $key"); + $this->assertEquals($value, $cache->get($key), "Should get correct value for key with postfix"); + + // Verify base IP is not affected + if (!$cache->has($ipv4)) { + $cache->set($ipv4, "base_ip_value"); + } + + $this->assertNotEquals($cache->get($ipv4), $cache->get($key), "Base IP and IP with postfix should have different values"); + } + + // Check all keys are still available (capacity not exceeded) + foreach ($postfixes as $postfix) { + $this->assertTrue($cache->has($ipv4 . $postfix), "All postfix keys should be available"); + } + $this->assertTrue($cache->has($ipv4), "Base IP should be available"); + } + + public function testCacheWithIPv6AndPostfixes() + { + // Create cache + $cache = new DefaultCache(5, 600); + + // Test IPv6 with various postfixes + $ipv6 = '2607:f8b0:4005:805::200e'; + $postfixes = ['_v1', '_latest', '_beta', '_test_123']; + + foreach ($postfixes as $postfix) { + $key = $ipv6 . $postfix; + $value = "value_for_$key"; + + // Set value with postfix + $cache->set($key, $value); + + // Verify it's in cache + $this->assertTrue($cache->has($key), "Cache should have key with postfix: $key"); + $this->assertEquals($value, $cache->get($key), "Should get correct value for key with postfix"); + } + + // Add the base IP to cache if not present + if (!$cache->has($ipv6)) { + $cache->set($ipv6, "base_ip_value"); + } + + // Ensure all keys are distinct and have different values + $this->assertEquals("base_ip_value", $cache->get($ipv6), "Base IP should have its own value"); + foreach ($postfixes as $postfix) { + $key = $ipv6 . $postfix; + $expected = "value_for_$key"; + $this->assertEquals($expected, $cache->get($key), "Each postfix should have its own value"); + } + } + + public function testCacheWithIPv6NotationsAndPostfixes() + { + // Create cache instance + $cache = new DefaultCache(100, 600); + + // Original IPv6 address + $standard_ip = "2607:f8b0:4005:805::200e"; + $postfixes = ['_v1', '_latest', '_beta', '_test_123']; + + // Variations with zeros + $variations = [ + "2607:f8b0:4005:805:0:0:0:200e", // Full form + "2607:f8b0:4005:805:0000:0000:0000:200e", // Full form with leading zeros + "2607:f8b0:4005:805:0000:00:00:200e", // Full form with few leading zeros + "2607:F8B0:4005:805::200E", // Uppercase notation + "2607:f8b0:4005:0805::200e", // Leading zero in a group + "2607:f8b0:4005:805:0::200e", // Partially expanded + "2607:f8b0:4005:805:0000::200e", // Full zeros in a second group + ]; + + foreach ($postfixes as $postfix) { + // Set cache with first postfix + $value = "value_for_$standard_ip"; + $cache->set($standard_ip . $postfix, $value); + foreach ($variations as $variation_id => $ip) { + $key = $ip . $postfix; + $this->assertTrue($cache->has($key), "Cache should have variation #$variation_id with postfix: $key"); + $this->assertEquals($value, $cache->get($key), "Should get correct value for key with postfix"); + } + } + + // Check that the base IP not cached + $this->assertFalse($cache->has($standard_ip), "Base IP should not be cached"); + } } diff --git a/tests/IPinfoTest.php b/tests/IPinfoTest.php index 2116f4e..fe75694 100644 --- a/tests/IPinfoTest.php +++ b/tests/IPinfoTest.php @@ -103,27 +103,39 @@ public function testLookup() $this->assertEquals($res->longitude, '-122.1175'); $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'], 'hosting'); - $this->assertEquals($res->company['name'], 'Google LLC'); - $this->assertEquals($res->company['domain'], 'google.com'); - $this->assertEquals($res->company['type'], 'hosting'); - $this->assertEquals($res->privacy['vpn'], false); - $this->assertEquals($res->privacy['proxy'], false); - $this->assertEquals($res->privacy['tor'], false); - $this->assertEquals($res->privacy['relay'], false); - $this->assertEquals($res->privacy['hosting'], true); - $this->assertEquals($res->privacy['service'], ''); - $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'); + if ($res->asn !== null) { + $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'], 'hosting'); + } + if ($res->company !== null) { + $this->assertEquals($res->company['name'], 'Google LLC'); + $this->assertEquals($res->company['domain'], 'google.com'); + $this->assertEquals($res->company['type'], 'hosting'); + } + if ($res->privacy !== null) { + $this->assertEquals($res->privacy['vpn'], false); + $this->assertEquals($res->privacy['proxy'], false); + $this->assertEquals($res->privacy['tor'], false); + $this->assertEquals($res->privacy['relay'], false); + if ($res->privacy['hosting'] !== null) { + $this->assertEquals($res->privacy['hosting'], true); + } + $this->assertEquals($res->privacy['service'], ''); + } + if ($res->abuse !== null) { + $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'); + } + if ($res->domains !== null) { + $this->assertEquals($res->domains['ip'], '8.8.8.8'); + } } } @@ -191,23 +203,45 @@ public function testGetBatchDetails() $this->assertArrayHasKey('9.9.9.9', $res); $this->assertArrayHasKey('10.10.10.10', $res); $this->assertEquals($res['8.8.8.8/hostname'], 'dns.google'); + $ipV4 = $res['4.4.4.4']; + $this->assertEquals($ipV4['ip'], '4.4.4.4'); + $this->assertEquals($ipV4['city'], 'Monroe'); + $this->assertEquals($ipV4['region'], 'Louisiana'); + $this->assertEquals($ipV4['country'], 'US'); + $this->assertEquals($ipV4['loc'], '32.5530,-92.0422'); + $this->assertEquals($ipV4['postal'], '71203'); + $this->assertEquals($ipV4['timezone'], 'America/Chicago'); + $this->assertEquals($ipV4['org'], 'AS3356 Level 3 Parent, LLC'); + } + } - $this->assertEquals($res['AS123'], [ - 'asn' => "AS123", - 'name' => "Air Force Systems Networking", - 'country' => "US", - 'allocated' => "1987-08-24", - 'registry' => "arin", - 'domain' => "af.mil", - 'num_ips' => 0, - 'type' => "inactive", - 'prefixes' => [], - 'prefixes6' => [], - 'peers' => null, - 'upstreams' => null, - 'downstreams' => null - ]); + public function testNetworkDetails() + { + $tok = getenv('IPINFO_TOKEN'); + if (!$tok) { + $this->markTestSkipped('IPINFO_TOKEN env var required'); } + + $h = new IPinfo($tok); + $res = $h->getDetails('AS123'); + + if ($res['error'] === "Token does not have access to this API") { + $this->markTestSkipped('Token does not have access to this API'); + } + + $this->assertEquals($res['asn'], 'AS123'); + $this->assertEquals($res['name'], 'Air Force Systems Networking'); + $this->assertEquals($res['country'], 'US'); + $this->assertEquals($res['allocated'], '1987-08-24'); + $this->assertEquals($res['registry'], 'arin'); + $this->assertEquals($res['domain'], 'af.mil'); + $this->assertEquals($res['num_ips'], 0); + $this->assertEquals($res['type'], 'inactive'); + $this->assertEquals($res['prefixes'], []); + $this->assertEquals($res['prefixes6'], []); + $this->assertEquals($res['peers'], null); + $this->assertEquals($res['upstreams'], null); + $this->assertEquals($res['downstreams'], null); } public function testBogonLocal4() @@ -227,4 +261,152 @@ public function testBogonLocal6() $this->assertEquals($res->ip, '2002:7f00::'); $this->assertTrue($res->bogon); } + + public function testIpv6Details() + { + $tok = getenv('IPINFO_TOKEN'); + if (!$tok) { + $this->markTestSkipped('IPINFO_TOKEN env var required'); + } + + $h = new IPinfo($tok); + $ip = "2607:f8b0:4005:805::200e"; + + // test multiple times for cache hits + for ($i = 0; $i < 5; $i++) { + $res = $h->getDetails($ip); + $this->assertEquals($res->ip, '2607:f8b0:4005:805::200e'); + $this->assertEquals($res->city, 'San Jose'); + $this->assertEquals($res->region, 'California'); + $this->assertEquals($res->country, 'US'); + $this->assertEquals($res->loc, '37.3394,-121.8950'); + $this->assertEquals($res->postal, '95025'); + $this->assertEquals($res->timezone, 'America/Los_Angeles'); + } + } + + public function testIPv6DifferentNotations() + { + $tok = getenv('IPINFO_TOKEN'); + if (!$tok) { + $this->markTestSkipped('IPINFO_TOKEN env var required'); + } + + $h = new IPinfo($tok); + + // Base IPv6 address with leading zeros in the second group + $standard_ip = "2607:00:4005:805::200e"; + $standard_result = $h->getDetails($standard_ip); + $this->assertEquals($standard_result->ip, '2607:00:4005:805::200e'); + $this->assertEquals($standard_result->city, 'Killarney'); + $this->assertEquals($standard_result->region, 'Manitoba'); + $this->assertEquals($standard_result->country, 'CA'); + $this->assertEquals($standard_result->loc, '49.1833,-99.6636'); + $this->assertEquals($standard_result->timezone, 'America/Winnipeg'); + + // Various notations of the same IPv6 address + $variations = [ + "2607:0:4005:805::200e", // Removed leading zeros in a second group + "2607:0000:4005:805::200e", // Full form with all zeros in the second group + "2607:0:4005:805:0:0:0:200e", // Expanded form without compressed zeros + "2607:0:4005:805:0000:0000:0000:200e", // Full expanded form + "2607:00:4005:805:0::200e", // Partially expanded + "2607:00:4005:805::200E", // Uppercase hex digits + "2607:00:4005:0805::200e" // Leading zero in a fourth group + ]; + + foreach ($variations as $id => $ip) { + // Test each variation + try { + $result = $h->getDetails($ip); + } + catch (\Exception $e) { + $this->fail("Failed to get details for IP #$id: $ip. Exception: " . $e->getMessage()); + } + + $this->assertEquals($ip, $result->ip, "IP #$id should match the requested IP : $ip"); + // Location data should be identical + $this->assertEquals($standard_result->city, $result->city, "City should match for IP: $ip"); + $this->assertEquals($standard_result->region, $result->region, "Region should match for IP: $ip"); + $this->assertEquals($standard_result->country, $result->country, "Country should match for IP: $ip"); + $this->assertEquals($standard_result->loc, $result->loc, "Location should match for IP: $ip"); + $this->assertEquals($standard_result->timezone, $result->timezone, "Timezone should match for IP: $ip"); + + // Binary comparison ensures the IP addresses are functionally identical + $this->assertEquals( + inet_ntop(inet_pton($standard_ip)), + inet_ntop(inet_pton($result->ip)), + "Normalized binary representation should match for IP: $ip" + ); + } + } + + public function testIPv6NotationsCaching() + { + $tok = getenv('IPINFO_TOKEN'); + if (!$tok) { + $this->markTestSkipped('IPINFO_TOKEN env var required'); + } + + // Create IPinfo instance with custom cache size + $h = new IPinfo($tok, ['cache_maxsize' => 10]); + + // Standard IPv6 address + $standard_ip = "2607:f8b0:4005:805::200e"; + + // Get details for standard IP (populate the cache) + $standard_result = $h->getDetails($standard_ip); + + // Create a mock for the Guzzle client to track API requests + $mock_guzzle = $this->createMock(\GuzzleHttp\Client::class); + + // The request method should never be called when IP is in cache + $mock_guzzle->expects($this->never()) + ->method('request'); + + // Replace the real Guzzle client with our mock + $reflectionClass = new \ReflectionClass($h); + $reflectionProperty = $reflectionClass->getProperty('http_client'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($h, $mock_guzzle); + + // Different notations of the same IPv6 address + $variations = [ + "2607:f8b0:4005:805:0:0:0:200e", // Full form + "2607:f8b0:4005:805:0000:0000:0000:200e", // Full form with leading zeros + "2607:f8b0:4005:0805::200e", // With leading zero in a group + "2607:f8b0:4005:805:0::200e", // Partially expanded + "2607:F8B0:4005:805::200E", // Uppercase notation + inet_ntop(inet_pton($standard_ip)) // Normalized form + ]; + + // Check cache hits for each variation + foreach ($variations as $ip) { + try { + // When requesting data for IP variations, API request should not occur + // because we expect a cache hit (normalized IP should be the same) + $result = $h->getDetails($ip); + + // Additionally, verify that data matches the original request + $this->assertEquals($standard_result->city, $result->city, "City should match for IP: $ip"); + $this->assertEquals($standard_result->country, $result->country, "Country should match for IP: $ip"); + + // Verify address normalization in binary representation + $this->assertEquals( + inet_ntop(inet_pton($standard_ip)), + inet_ntop(inet_pton($ip)), + "Normalized binary representation should match for IP: $ip" + ); + } catch (\Exception $e) { + $this->fail("Cache hit failed for IP notation: $ip. Exception: " . $e->getMessage()); + } + } + + // Directly check if the key exists in cache + $h->getDetails($standard_ip); + + // The normalized IP should exist in cache + $normalized_ip = inet_ntop(inet_pton($standard_ip)); + $h->getDetails($normalized_ip); + } }