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..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'); } @@ -405,4 +409,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, []); + } }