Skip to content

Commit 6938950

Browse files
rvalitovmax-ipinfo
authored andcommitted
[add] proper caching of IPv6 with different notations
1 parent dac0b62 commit 6938950

File tree

3 files changed

+241
-11
lines changed

3 files changed

+241
-11
lines changed

src/cache/DefaultCache.php

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,20 @@ public function set(string $name, $value)
6363
* @param string $ip_address IP address to lookup in cache.
6464
* @return mixed IP address data.
6565
*/
66-
public function get(string $name)
66+
public function get(string $ip_address)
6767
{
68-
$name = $this->sanitizeName($name);
68+
$sanitizeName = $this->sanitizeName($ip_address);
69+
$result = $this->cache->getItem($sanitizeName)->get();
70+
if (is_array($result) && array_key_exists("ip", $result)) {
71+
/**
72+
* The IPv6 may have different notation and we don't know which one is cached.
73+
* We want to give the user the same notation as the one used in his request which may be different from
74+
* the one used in the cache.
75+
*/
76+
$result["ip"] = $this->getIpAddress($ip_address);
77+
}
6978

70-
return $this->cache->getItem($name)->get();
79+
return $result;
7180
}
7281

7382
/**
@@ -86,13 +95,33 @@ private function manageSize()
8695
}
8796
}
8897

98+
private function getIpAddress(string $name): string
99+
{
100+
// The $name has the version postfix applied, we need to extract the IP address without it
101+
$parts = explode('_', $name);
102+
return $parts[0];
103+
}
104+
89105
/**
90106
* Remove forbidden characters from cache keys
91107
*/
92108
private function sanitizeName(string $name): string
93109
{
94-
$forbiddenCharacters = str_split(CacheItem::RESERVED_CHARACTERS);
110+
// The $name has the version postfix applied, we need to extract the IP address without it
111+
$parts = explode('_', $name);
112+
$ip = $parts[0];
113+
try {
114+
// Attempt to normalize the IPv6 address
115+
$binary = @inet_pton($ip); // Convert to 16-byte binary
116+
if ($binary !== false && strlen($binary) === 16) { // Valid IPv6
117+
$ip = inet_ntop($binary); // Convert to full notation (e.g., 2001:0db8:...)
118+
}
119+
$name = $ip . '_' . implode('_', array_slice($parts, 1));
120+
} catch (\Exception) {
121+
// If invalid, proceed with original input
122+
}
95123

96-
return str_replace($forbiddenCharacters, "", $name);
124+
$forbiddenCharacters = str_split(CacheItem::RESERVED_CHARACTERS);
125+
return str_replace($forbiddenCharacters, '^', $name);
97126
}
98127
}

tests/DefaultCacheTest.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,136 @@ public function testTimeToLiveExceeded()
8181
sleep(2);
8282
$this->assertEquals(null, $cache->get($key));
8383
}
84+
85+
public function testCacheWithIPv6DifferentNotations()
86+
{
87+
// Create cache instance
88+
$cache = new DefaultCache(10, 600);
89+
90+
// Original IPv6 address
91+
$standard_ip = "2607:f8b0:4005:805::200e";
92+
$standard_value = "standard_value";
93+
$cache->set($standard_ip, $standard_value);
94+
95+
// Variations with zeros
96+
$variations = [
97+
"2607:f8b0:4005:805:0:0:0:200e", // Full form
98+
"2607:f8b0:4005:805:0000:0000:0000:200e", // Full form with leading zeros
99+
"2607:f8b0:4005:805:0000:00:00:200e", // Full form with few leading zeros
100+
"2607:F8B0:4005:805::200E", // Uppercase notation
101+
"2607:f8b0:4005:0805::200e", // Leading zero in a group
102+
"2607:f8b0:4005:805:0::200e", // Partially expanded
103+
"2607:f8b0:4005:805:0000::200e", // Full zeros in a second group
104+
];
105+
106+
// DefaultCache does normalize IPs, so we need to check if the cache has the same value
107+
foreach ($variations as $ip) {
108+
$this->assertTrue($cache->has($ip), "Cache should have variation: $ip");
109+
}
110+
}
111+
112+
public function testDefaultCacheWithIPv4AndPostfixes()
113+
{
114+
// Create cache
115+
$cache = new DefaultCache(5, 600);
116+
117+
// Test IPv4 with various postfixes
118+
$ipv4 = '8.8.8.8';
119+
$postfixes = ['_v1', '_latest', '_beta', '_test_123'];
120+
121+
foreach ($postfixes as $postfix) {
122+
$key = $ipv4 . $postfix;
123+
$value = "value_for_$key";
124+
125+
// Set value with postfix
126+
$cache->set($key, $value);
127+
128+
// Verify it's in cache
129+
$this->assertTrue($cache->has($key), "Cache should have key with postfix: $key");
130+
$this->assertEquals($value, $cache->get($key), "Should get correct value for key with postfix");
131+
132+
// Verify base IP is not affected
133+
if (!$cache->has($ipv4)) {
134+
$cache->set($ipv4, "base_ip_value");
135+
}
136+
137+
$this->assertNotEquals($cache->get($ipv4), $cache->get($key), "Base IP and IP with postfix should have different values");
138+
}
139+
140+
// Check all keys are still available (capacity not exceeded)
141+
foreach ($postfixes as $postfix) {
142+
$this->assertTrue($cache->has($ipv4 . $postfix), "All postfix keys should be available");
143+
}
144+
$this->assertTrue($cache->has($ipv4), "Base IP should be available");
145+
}
146+
147+
public function testCacheWithIPv6AndPostfixes()
148+
{
149+
// Create cache
150+
$cache = new DefaultCache(5, 600);
151+
152+
// Test IPv6 with various postfixes
153+
$ipv6 = '2607:f8b0:4005:805::200e';
154+
$postfixes = ['_v1', '_latest', '_beta', '_test_123'];
155+
156+
foreach ($postfixes as $postfix) {
157+
$key = $ipv6 . $postfix;
158+
$value = "value_for_$key";
159+
160+
// Set value with postfix
161+
$cache->set($key, $value);
162+
163+
// Verify it's in cache
164+
$this->assertTrue($cache->has($key), "Cache should have key with postfix: $key");
165+
$this->assertEquals($value, $cache->get($key), "Should get correct value for key with postfix");
166+
}
167+
168+
// Add the base IP to cache if not present
169+
if (!$cache->has($ipv6)) {
170+
$cache->set($ipv6, "base_ip_value");
171+
}
172+
173+
// Ensure all keys are distinct and have different values
174+
$this->assertEquals("base_ip_value", $cache->get($ipv6), "Base IP should have its own value");
175+
foreach ($postfixes as $postfix) {
176+
$key = $ipv6 . $postfix;
177+
$expected = "value_for_$key";
178+
$this->assertEquals($expected, $cache->get($key), "Each postfix should have its own value");
179+
}
180+
}
181+
182+
public function testCacheWithIPv6NotationsAndPostfixes()
183+
{
184+
// Create cache instance
185+
$cache = new DefaultCache(100, 600);
186+
187+
// Original IPv6 address
188+
$standard_ip = "2607:f8b0:4005:805::200e";
189+
$postfixes = ['_v1', '_latest', '_beta', '_test_123'];
190+
191+
// Variations with zeros
192+
$variations = [
193+
"2607:f8b0:4005:805:0:0:0:200e", // Full form
194+
"2607:f8b0:4005:805:0000:0000:0000:200e", // Full form with leading zeros
195+
"2607:f8b0:4005:805:0000:00:00:200e", // Full form with few leading zeros
196+
"2607:F8B0:4005:805::200E", // Uppercase notation
197+
"2607:f8b0:4005:0805::200e", // Leading zero in a group
198+
"2607:f8b0:4005:805:0::200e", // Partially expanded
199+
"2607:f8b0:4005:805:0000::200e", // Full zeros in a second group
200+
];
201+
202+
foreach ($postfixes as $postfix) {
203+
// Set cache with first postfix
204+
$value = "value_for_$standard_ip";
205+
$cache->set($standard_ip . $postfix, $value);
206+
foreach ($variations as $variation_id => $ip) {
207+
$key = $ip . $postfix;
208+
$this->assertTrue($cache->has($key), "Cache should have variation #$variation_id with postfix: $key");
209+
$this->assertEquals($value, $cache->get($key), "Should get correct value for key with postfix");
210+
}
211+
}
212+
213+
// Check that the base IP not cached
214+
$this->assertFalse($cache->has($standard_ip), "Base IP should not be cached");
215+
}
84216
}

tests/IPinfoTest.php

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -306,25 +306,25 @@ public function testIPv6DifferentNotations()
306306

307307
// Various notations of the same IPv6 address
308308
$variations = [
309-
"2607:0:4005:805::200e", // Removed leading zeros in second group
310-
"2607:0000:4005:805::200e", // Full form with all zeros in second group
309+
"2607:0:4005:805::200e", // Removed leading zeros in a second group
310+
"2607:0000:4005:805::200e", // Full form with all zeros in the second group
311311
"2607:0:4005:805:0:0:0:200e", // Expanded form without compressed zeros
312312
"2607:0:4005:805:0000:0000:0000:200e", // Full expanded form
313313
"2607:00:4005:805:0::200e", // Partially expanded
314314
"2607:00:4005:805::200E", // Uppercase hex digits
315-
"2607:00:4005:0805::200e" // Leading zero in fourth group
315+
"2607:00:4005:0805::200e" // Leading zero in a fourth group
316316
];
317317

318-
foreach ($variations as $ip) {
318+
foreach ($variations as $id => $ip) {
319319
// Test each variation
320320
try {
321321
$result = $h->getDetails($ip);
322322
}
323323
catch (\Exception $e) {
324-
$this->fail("Failed to get details for IP: $ip. Exception: " . $e->getMessage());
324+
$this->fail("Failed to get details for IP #$id: $ip. Exception: " . $e->getMessage());
325325
}
326326

327-
$this->assertEquals($ip, $result->ip);
327+
$this->assertEquals($ip, $result->ip, "IP #$id should match the requested IP : $ip");
328328
// Location data should be identical
329329
$this->assertEquals($standard_result->city, $result->city, "City should match for IP: $ip");
330330
$this->assertEquals($standard_result->region, $result->region, "Region should match for IP: $ip");
@@ -340,4 +340,73 @@ public function testIPv6DifferentNotations()
340340
);
341341
}
342342
}
343+
344+
public function testIPv6NotationsCaching()
345+
{
346+
$tok = getenv('IPINFO_TOKEN');
347+
if (!$tok) {
348+
$this->markTestSkipped('IPINFO_TOKEN env var required');
349+
}
350+
351+
// Create IPinfo instance with custom cache size
352+
$h = new IPinfo($tok, ['cache_maxsize' => 10]);
353+
354+
// Standard IPv6 address
355+
$standard_ip = "2607:f8b0:4005:805::200e";
356+
357+
// Get details for standard IP (populate the cache)
358+
$standard_result = $h->getDetails($standard_ip);
359+
360+
// Create a mock for the Guzzle client to track API requests
361+
$mock_guzzle = $this->createMock(\GuzzleHttp\Client::class);
362+
363+
// The request method should never be called when IP is in cache
364+
$mock_guzzle->expects($this->never())
365+
->method('request');
366+
367+
// Replace the real Guzzle client with our mock
368+
$reflectionClass = new \ReflectionClass($h);
369+
$reflectionProperty = $reflectionClass->getProperty('http_client');
370+
$reflectionProperty->setAccessible(true);
371+
$reflectionProperty->setValue($h, $mock_guzzle);
372+
373+
// Different notations of the same IPv6 address
374+
$variations = [
375+
"2607:f8b0:4005:805:0:0:0:200e", // Full form
376+
"2607:f8b0:4005:805:0000:0000:0000:200e", // Full form with leading zeros
377+
"2607:f8b0:4005:0805::200e", // With leading zero in a group
378+
"2607:f8b0:4005:805:0::200e", // Partially expanded
379+
"2607:F8B0:4005:805::200E", // Uppercase notation
380+
inet_ntop(inet_pton($standard_ip)) // Normalized form
381+
];
382+
383+
// Check cache hits for each variation
384+
foreach ($variations as $ip) {
385+
try {
386+
// When requesting data for IP variations, API request should not occur
387+
// because we expect a cache hit (normalized IP should be the same)
388+
$result = $h->getDetails($ip);
389+
390+
// Additionally, verify that data matches the original request
391+
$this->assertEquals($standard_result->city, $result->city, "City should match for IP: $ip");
392+
$this->assertEquals($standard_result->country, $result->country, "Country should match for IP: $ip");
393+
394+
// Verify address normalization in binary representation
395+
$this->assertEquals(
396+
inet_ntop(inet_pton($standard_ip)),
397+
inet_ntop(inet_pton($ip)),
398+
"Normalized binary representation should match for IP: $ip"
399+
);
400+
} catch (\Exception $e) {
401+
$this->fail("Cache hit failed for IP notation: $ip. Exception: " . $e->getMessage());
402+
}
403+
}
404+
405+
// Directly check if the key exists in cache
406+
$h->getDetails($standard_ip);
407+
408+
// The normalized IP should exist in cache
409+
$normalized_ip = inet_ntop(inet_pton($standard_ip));
410+
$h->getDetails($normalized_ip);
411+
}
343412
}

0 commit comments

Comments
 (0)