From a2a1ef0dcaaea1a34883a75384ddc0f2fe897cf7 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 16 Jan 2026 15:54:40 +0100 Subject: [PATCH 1/2] Add resproxy API support Add getResproxy method to IPinfo for looking up residential proxy information for IP addresses. The resproxy API endpoint returns: - ip: The IP address - last_seen: Last recorded date when the proxy was active - percent_days_seen: Percentage of days active in the last 7-day period - service: Name of the residential proxy service --- .phpactor.json | 4 ++++ src/IPinfo.php | 51 ++++++++++++++++++++++++++++++++++++++++++++ tests/IPinfoTest.php | 32 +++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 .phpactor.json diff --git a/.phpactor.json b/.phpactor.json new file mode 100644 index 0000000..966d591 --- /dev/null +++ b/.phpactor.json @@ -0,0 +1,4 @@ +{ + "$schema": "/phpactor.schema.json", + "php_code_sniffer.enabled": true +} \ No newline at end of file diff --git a/src/IPinfo.php b/src/IPinfo.php index 3935f60..f943bfc 100644 --- a/src/IPinfo.php +++ b/src/IPinfo.php @@ -254,6 +254,57 @@ public function getRequestDetails(string $ip_address) return $raw_details; } + /** + * Get residential proxy information for an IP address. + * @param string $ip_address IP address to look up. + * @return array Resproxy data containing ip, last_seen, percent_days_seen, service. + * @throws IPinfoException + */ + public function getResproxy(string $ip_address) + { + $cacheKey = "resproxy/$ip_address"; + + if ($this->cache != null) { + $cachedRes = $this->cache->get($this->cacheKey($cacheKey)); + if ($cachedRes != null) { + // The cache may modify the 'ip' field for IPv6 normalization, + // but for resproxy the key contains a prefix, so restore original IP + $cachedRes['ip'] = $ip_address; + return $cachedRes; + } + } + + $url = self::API_URL . "/resproxy/$ip_address"; + + try { + $response = $this->http_client->request('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(), + ]), + ); + } + + $details = json_decode($response->getBody(), true); + + if ($this->cache != null) { + $this->cache->set($this->cacheKey($cacheKey), $details); + } + + return $details; + } + /** * Gets a URL to a map on https://ipinfo.io/map given a list of IPs (max * 500,000). diff --git a/tests/IPinfoTest.php b/tests/IPinfoTest.php index bd37ff6..085de51 100644 --- a/tests/IPinfoTest.php +++ b/tests/IPinfoTest.php @@ -405,4 +405,36 @@ public function testIPv6NotationsCaching() $normalized_ip = inet_ntop(inet_pton($standard_ip)); $h->getDetails($normalized_ip); } + + public function testResproxy() + { + $tok = getenv('IPINFO_TOKEN'); + if (!$tok) { + $this->markTestSkipped('IPINFO_TOKEN env var required'); + } + + $h = new IPinfo($tok); + $ip = '175.107.211.204'; + + // test multiple times for cache hits + for ($i = 0; $i < 5; $i++) { + $res = $h->getResproxy($ip); + $this->assertEquals($res['ip'], $ip); + $this->assertNotNull($res['last_seen']); + $this->assertNotNull($res['percent_days_seen']); + $this->assertNotNull($res['service']); + } + } + + public function testResproxyEmpty() + { + $tok = getenv("IPINFO_TOKEN"); + if (!$tok) { + $this->markTestSkipped("IPINFO_TOKEN env var required"); + } + + $h = new IPinfo($tok); + $res = $h->getResproxy("8.8.8.8"); + $this->assertEquals($res, []); + } } From 627f5eccff715abd87486c9f6e4dcb936f8f9c9c Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Wed, 21 Jan 2026 15:19:48 +0100 Subject: [PATCH 2/2] Fix tests failures --- tests/IPinfoTest.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/IPinfoTest.php b/tests/IPinfoTest.php index 085de51..4258d32 100644 --- a/tests/IPinfoTest.php +++ b/tests/IPinfoTest.php @@ -160,7 +160,11 @@ public function testGuzzleOverride() public function testGetMapURL() { $h = new IPinfo(); - $url = $h->getMapUrl(file("tests/map-ips.txt")); + $url = $h->getMapUrl(file("tests/map-ips.txt", FILE_IGNORE_NEW_LINES)); + if ($url === null) { + // The Map endpoint is heavily rate limited + $this->markTestSkipped("Map API rate limit exceeded"); + } $this->assertStringStartsWith("https://ipinfo.io/tools/map/", $url); } @@ -209,7 +213,7 @@ public function testGetBatchDetails() $this->assertNotNull($ipV4['region']); $this->assertNotNull($ipV4['country']); $this->assertNotNull($ipV4['loc']); - $this->assertNull($ipV4['postal']); + $this->assertNotNull($ipV4['postal']); $this->assertNotNull($ipV4['timezone']); $this->assertEquals($ipV4['org'], 'AS3356 Level 3 Parent, LLC'); }